cleanup 1
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -79,12 +79,6 @@
|
|||||||
<version>4.2.0</version>
|
<version>4.2.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- WebSocket -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Telegram Bot API -->
|
<!-- Telegram Bot API -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.telegram</groupId>
|
<groupId>org.telegram</groupId>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
"/api/check_user/**", // User check endpoint for external applications (open endpoint)
|
"/api/check_user/**", // User check endpoint for external applications (open endpoint)
|
||||||
"/api/deposit_webhook/**", // 3rd party deposit completion webhook (token in path, no auth)
|
"/api/deposit_webhook/**", // 3rd party deposit completion webhook (token in path, no auth)
|
||||||
"/api/notify_broadcast/**", // Notify broadcast start/stop (token in path, no auth)
|
"/api/notify_broadcast/**", // Notify broadcast start/stop (token in path, no auth)
|
||||||
"/api/remotebet/**", // Remote bet: token + feature switch protected, no user auth
|
|
||||||
"/api/admin/**" // Admin endpoints are handled by Spring Security
|
"/api/admin/**" // Admin endpoints are handled by Spring Security
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
package com.lottery.lottery.config;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.UserA;
|
|
||||||
import com.lottery.lottery.security.UserContext;
|
|
||||||
import com.lottery.lottery.service.SessionService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.messaging.Message;
|
|
||||||
import org.springframework.messaging.MessageChannel;
|
|
||||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
|
||||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
|
||||||
import org.springframework.messaging.support.ChannelInterceptor;
|
|
||||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.security.Principal;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class WebSocketAuthInterceptor implements ChannelInterceptor {
|
|
||||||
|
|
||||||
private final SessionService sessionService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
|
||||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
|
||||||
|
|
||||||
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
|
|
||||||
// Extract Bearer token from headers
|
|
||||||
List<String> authHeaders = accessor.getNativeHeader("Authorization");
|
|
||||||
String token = null;
|
|
||||||
|
|
||||||
if (authHeaders != null && !authHeaders.isEmpty()) {
|
|
||||||
String authHeader = authHeaders.get(0);
|
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
|
||||||
token = authHeader.substring(7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check query parameter (for SockJS fallback)
|
|
||||||
if (token == null) {
|
|
||||||
String query = accessor.getFirstNativeHeader("query");
|
|
||||||
if (query != null && query.contains("token=")) {
|
|
||||||
int tokenStart = query.indexOf("token=") + 6;
|
|
||||||
int tokenEnd = query.indexOf("&", tokenStart);
|
|
||||||
if (tokenEnd == -1) {
|
|
||||||
tokenEnd = query.length();
|
|
||||||
}
|
|
||||||
token = query.substring(tokenStart, tokenEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token == null || token.isBlank()) {
|
|
||||||
log.warn("WebSocket connection rejected: No token provided");
|
|
||||||
throw new SecurityException("Authentication required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate token and get user
|
|
||||||
var userOpt = sessionService.getUserBySession(token);
|
|
||||||
if (userOpt.isEmpty()) {
|
|
||||||
log.warn("WebSocket connection rejected: Invalid token");
|
|
||||||
throw new SecurityException("Invalid authentication token");
|
|
||||||
}
|
|
||||||
|
|
||||||
UserA user = userOpt.get();
|
|
||||||
accessor.setUser(new StompPrincipal(user.getId(), user));
|
|
||||||
UserContext.set(user);
|
|
||||||
|
|
||||||
log.debug("WebSocket connection authenticated for user {}", user.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
|
|
||||||
UserContext.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple principal class to store user info
|
|
||||||
public static class StompPrincipal implements Principal {
|
|
||||||
private final Integer userId;
|
|
||||||
private final UserA user;
|
|
||||||
|
|
||||||
public StompPrincipal(Integer userId, UserA user) {
|
|
||||||
this.userId = userId;
|
|
||||||
this.user = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getUserId() {
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserA getUser() {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return String.valueOf(userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package com.lottery.lottery.config;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
|
||||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
|
||||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
|
||||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSocketMessageBroker
|
|
||||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|
||||||
|
|
||||||
private final WebSocketAuthInterceptor authInterceptor;
|
|
||||||
|
|
||||||
@Value("${app.websocket.allowed-origins:https://win-spin.live,https://lottery-fe-production.up.railway.app,https://web.telegram.org,https://webk.telegram.org,https://webz.telegram.org}")
|
|
||||||
private String allowedOrigins;
|
|
||||||
|
|
||||||
public WebSocketConfig(WebSocketAuthInterceptor authInterceptor) {
|
|
||||||
this.authInterceptor = authInterceptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
|
||||||
// Enable simple broker for sending messages to clients
|
|
||||||
config.enableSimpleBroker("/topic", "/queue");
|
|
||||||
// Prefix for messages from client to server
|
|
||||||
config.setApplicationDestinationPrefixes("/app");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
|
||||||
// Parse allowed origins from configuration
|
|
||||||
// Spring's setAllowedOriginPatterns uses Ant-style patterns, not regex
|
|
||||||
// For exact matches, use the URL as-is
|
|
||||||
// For subdomain matching, use https://*.example.com
|
|
||||||
List<String> origins = Arrays.asList(allowedOrigins.split(","));
|
|
||||||
String[] originPatterns = origins.stream()
|
|
||||||
.map(String::trim)
|
|
||||||
.filter(origin -> !origin.isEmpty())
|
|
||||||
.toArray(String[]::new);
|
|
||||||
|
|
||||||
log.info("[WEBSOCKET] Configuring WebSocket endpoint /ws with allowed origins: {}", Arrays.toString(originPatterns));
|
|
||||||
|
|
||||||
// WebSocket endpoint - clients connect here
|
|
||||||
registry.addEndpoint("/ws")
|
|
||||||
.setAllowedOriginPatterns(originPatterns) // Restricted to configured domains
|
|
||||||
.withSockJS();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
|
||||||
registration.interceptors(authInterceptor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
package com.lottery.lottery.config;
|
|
||||||
|
|
||||||
import com.lottery.lottery.dto.GameRoomStateDto;
|
|
||||||
import com.lottery.lottery.service.GameRoomService;
|
|
||||||
import com.lottery.lottery.service.RoomConnectionService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
|
||||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
|
|
||||||
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
|
|
||||||
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
|
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class WebSocketSubscriptionListener {
|
|
||||||
|
|
||||||
private final GameRoomService gameRoomService;
|
|
||||||
private final SimpMessagingTemplate messagingTemplate;
|
|
||||||
private final RoomConnectionService roomConnectionService;
|
|
||||||
|
|
||||||
// Pattern to match room subscription: /topic/room/{roomNumber}
|
|
||||||
private static final Pattern ROOM_SUBSCRIPTION_PATTERN = Pattern.compile("/topic/room/(\\d+)");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listens for WebSocket subscription events.
|
|
||||||
* When a client subscribes to a room topic, sends the current room state immediately.
|
|
||||||
*/
|
|
||||||
@EventListener
|
|
||||||
public void handleSubscription(SessionSubscribeEvent event) {
|
|
||||||
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
|
|
||||||
String destination = accessor.getDestination();
|
|
||||||
|
|
||||||
if (destination == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a room subscription
|
|
||||||
Matcher matcher = ROOM_SUBSCRIPTION_PATTERN.matcher(destination);
|
|
||||||
if (matcher.matches()) {
|
|
||||||
try {
|
|
||||||
Integer roomNumber = Integer.parseInt(matcher.group(1));
|
|
||||||
|
|
||||||
// Get the user ID from the principal
|
|
||||||
Object principal = accessor.getUser();
|
|
||||||
Integer userId = null;
|
|
||||||
if (principal instanceof WebSocketAuthInterceptor.StompPrincipal) {
|
|
||||||
userId = ((WebSocketAuthInterceptor.StompPrincipal) principal).getUserId();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session ID
|
|
||||||
String sessionId = accessor.getSessionId();
|
|
||||||
|
|
||||||
log.info("Client subscribed to room {} (userId: {}, sessionId: {})", roomNumber, userId, sessionId);
|
|
||||||
|
|
||||||
// Register session for disconnect tracking
|
|
||||||
if (sessionId != null && userId != null) {
|
|
||||||
roomConnectionService.registerSession(sessionId, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track room-level connection (not just round participation)
|
|
||||||
if (userId != null && sessionId != null) {
|
|
||||||
roomConnectionService.addUserToRoom(userId, roomNumber, sessionId);
|
|
||||||
} else {
|
|
||||||
log.warn("Cannot track room connection: userId={}, sessionId={}", userId, sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current room state and send it to the subscribing client
|
|
||||||
// This ensures client gets authoritative state immediately on subscribe
|
|
||||||
GameRoomStateDto state = gameRoomService.getRoomState(roomNumber);
|
|
||||||
|
|
||||||
// Send state directly to the destination (room topic)
|
|
||||||
// This will be received by the subscribing client
|
|
||||||
messagingTemplate.convertAndSend(destination, state);
|
|
||||||
|
|
||||||
log.debug("Sent initial room state to subscriber: room={}, phase={}, participants={}, connectedUsers={}",
|
|
||||||
roomNumber, state.getPhase(),
|
|
||||||
state.getParticipants() != null ? state.getParticipants().size() : 0,
|
|
||||||
state.getConnectedUsers());
|
|
||||||
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.warn("Invalid room number in subscription destination: {}", destination);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error sending initial state for room subscription: {}", destination, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listens for WebSocket unsubscribe events.
|
|
||||||
* When a client unsubscribes from a room topic, removes them from room connections.
|
|
||||||
*/
|
|
||||||
@EventListener
|
|
||||||
public void handleUnsubscribe(SessionUnsubscribeEvent event) {
|
|
||||||
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
|
|
||||||
String destination = accessor.getDestination();
|
|
||||||
|
|
||||||
// Skip if destination is null (Spring WebSocket sometimes sends unsubscribe events without destination during cleanup)
|
|
||||||
if (destination == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Unsubscribe event received for destination: {}", destination);
|
|
||||||
|
|
||||||
// Check if this is a room unsubscription
|
|
||||||
Matcher matcher = ROOM_SUBSCRIPTION_PATTERN.matcher(destination);
|
|
||||||
if (matcher.matches()) {
|
|
||||||
try {
|
|
||||||
Integer roomNumber = Integer.parseInt(matcher.group(1));
|
|
||||||
|
|
||||||
// Get the user ID from the principal
|
|
||||||
Object principal = accessor.getUser();
|
|
||||||
Integer userId = null;
|
|
||||||
if (principal instanceof WebSocketAuthInterceptor.StompPrincipal) {
|
|
||||||
userId = ((WebSocketAuthInterceptor.StompPrincipal) principal).getUserId();
|
|
||||||
} else {
|
|
||||||
log.warn("Unsubscribe event: principal is not StompPrincipal, type: {}",
|
|
||||||
principal != null ? principal.getClass().getName() : "null");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session ID
|
|
||||||
String sessionId = accessor.getSessionId();
|
|
||||||
|
|
||||||
if (userId != null && sessionId != null) {
|
|
||||||
log.info("Client unsubscribed from room {} (userId: {}, sessionId: {})", roomNumber, userId, sessionId);
|
|
||||||
roomConnectionService.removeUserFromRoom(userId, roomNumber, sessionId);
|
|
||||||
} else {
|
|
||||||
log.warn("Unsubscribe event: userId or sessionId is null for destination: {} (userId: {}, sessionId: {})",
|
|
||||||
destination, userId, sessionId);
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.warn("Invalid room number in unsubscription destination: {}", destination);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error handling room unsubscription: {}", destination, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.debug("Unsubscribe event destination does not match room pattern: {}", destination);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listens for WebSocket disconnect events.
|
|
||||||
* When a client disconnects completely, removes them from all rooms.
|
|
||||||
*/
|
|
||||||
@EventListener
|
|
||||||
public void handleDisconnect(SessionDisconnectEvent event) {
|
|
||||||
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
|
|
||||||
String sessionId = accessor.getSessionId();
|
|
||||||
|
|
||||||
// Try to get user ID from principal first
|
|
||||||
Object principal = accessor.getUser();
|
|
||||||
Integer userId = null;
|
|
||||||
if (principal instanceof WebSocketAuthInterceptor.StompPrincipal) {
|
|
||||||
userId = ((WebSocketAuthInterceptor.StompPrincipal) principal).getUserId();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId != null && sessionId != null) {
|
|
||||||
log.info("Client disconnected (userId: {}, sessionId: {}), removing session from all rooms", userId, sessionId);
|
|
||||||
// Remove only this specific session from all rooms
|
|
||||||
roomConnectionService.removeUserFromAllRooms(userId, sessionId);
|
|
||||||
// Also remove session mapping
|
|
||||||
roomConnectionService.removeSession(sessionId);
|
|
||||||
} else if (sessionId != null) {
|
|
||||||
// Principal might be lost, try to get userId from session mapping
|
|
||||||
log.info("Client disconnected (sessionId: {}), principal lost, using session mapping", sessionId);
|
|
||||||
roomConnectionService.removeUserFromAllRoomsBySession(sessionId);
|
|
||||||
} else {
|
|
||||||
log.warn("Disconnect event: both userId and sessionId are null, cannot remove from rooms");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -2,7 +2,6 @@ package com.lottery.lottery.controller;
|
|||||||
|
|
||||||
import com.lottery.lottery.model.Payment;
|
import com.lottery.lottery.model.Payment;
|
||||||
import com.lottery.lottery.model.Payout;
|
import com.lottery.lottery.model.Payout;
|
||||||
import com.lottery.lottery.repository.GameRoundRepository;
|
|
||||||
import com.lottery.lottery.repository.PaymentRepository;
|
import com.lottery.lottery.repository.PaymentRepository;
|
||||||
import com.lottery.lottery.repository.PayoutRepository;
|
import com.lottery.lottery.repository.PayoutRepository;
|
||||||
import com.lottery.lottery.repository.UserARepository;
|
import com.lottery.lottery.repository.UserARepository;
|
||||||
@@ -30,7 +29,6 @@ public class AdminAnalyticsController {
|
|||||||
private final UserARepository userARepository;
|
private final UserARepository userARepository;
|
||||||
private final PaymentRepository paymentRepository;
|
private final PaymentRepository paymentRepository;
|
||||||
private final PayoutRepository payoutRepository;
|
private final PayoutRepository payoutRepository;
|
||||||
private final GameRoundRepository gameRoundRepository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get revenue and payout time series data for charts.
|
* Get revenue and payout time series data for charts.
|
||||||
@@ -181,14 +179,11 @@ public class AdminAnalyticsController {
|
|||||||
// Count active players (logged in) in this period
|
// Count active players (logged in) in this period
|
||||||
long activePlayers = userARepository.countByDateLoginBetween(periodStartTs, periodEndTs);
|
long activePlayers = userARepository.countByDateLoginBetween(periodStartTs, periodEndTs);
|
||||||
|
|
||||||
// Count rounds resolved in this period
|
|
||||||
long rounds = gameRoundRepository.countByResolvedAtBetween(current, periodEnd);
|
|
||||||
|
|
||||||
Map<String, Object> point = new HashMap<>();
|
Map<String, Object> point = new HashMap<>();
|
||||||
point.put("date", current.getEpochSecond());
|
point.put("date", current.getEpochSecond());
|
||||||
point.put("newUsers", newUsers);
|
point.put("newUsers", newUsers);
|
||||||
point.put("activePlayers", activePlayers);
|
point.put("activePlayers", activePlayers);
|
||||||
point.put("rounds", rounds);
|
point.put("rounds", 0L);
|
||||||
|
|
||||||
dataPoints.add(point);
|
dataPoints.add(point);
|
||||||
|
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
package com.lottery.lottery.controller;
|
|
||||||
|
|
||||||
import com.lottery.lottery.dto.AdminBotConfigDto;
|
|
||||||
import com.lottery.lottery.dto.AdminBotConfigRequest;
|
|
||||||
import com.lottery.lottery.service.AdminBotConfigService;
|
|
||||||
import com.lottery.lottery.service.ConfigurationService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/admin/bots")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@PreAuthorize("hasRole('ADMIN')")
|
|
||||||
public class AdminBotConfigController {
|
|
||||||
|
|
||||||
private final AdminBotConfigService adminBotConfigService;
|
|
||||||
private final ConfigurationService configurationService;
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
public ResponseEntity<List<AdminBotConfigDto>> list() {
|
|
||||||
return ResponseEntity.ok(adminBotConfigService.listAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
public ResponseEntity<AdminBotConfigDto> getById(@PathVariable Integer id) {
|
|
||||||
Optional<AdminBotConfigDto> dto = adminBotConfigService.getById(id);
|
|
||||||
return dto.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
public ResponseEntity<?> create(@Valid @RequestBody AdminBotConfigRequest request) {
|
|
||||||
try {
|
|
||||||
AdminBotConfigDto created = adminBotConfigService.create(request);
|
|
||||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
public ResponseEntity<?> update(@PathVariable Integer id, @Valid @RequestBody AdminBotConfigRequest request) {
|
|
||||||
try {
|
|
||||||
Optional<AdminBotConfigDto> updated = adminBotConfigService.update(id, request);
|
|
||||||
return updated.map(ResponseEntity::ok)
|
|
||||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
public ResponseEntity<Void> delete(@PathVariable Integer id) {
|
|
||||||
boolean deleted = adminBotConfigService.delete(id);
|
|
||||||
return deleted ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shuffle time windows for bots that have the given room enabled.
|
|
||||||
* Redistributes the same set of time windows randomly across those bots.
|
|
||||||
*/
|
|
||||||
@PostMapping("/shuffle")
|
|
||||||
public ResponseEntity<?> shuffleTimeWindows(@RequestParam int roomNumber) {
|
|
||||||
if (roomNumber != 2 && roomNumber != 3) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "roomNumber must be 2 or 3"));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
adminBotConfigService.shuffleTimeWindowsForRoom(roomNumber);
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/settings")
|
|
||||||
public ResponseEntity<Map<String, Integer>> getBotSettings() {
|
|
||||||
return ResponseEntity.ok(Map.of("maxParticipantsBeforeBotJoin", configurationService.getMaxParticipantsBeforeBotJoin()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PatchMapping("/settings")
|
|
||||||
public ResponseEntity<?> updateBotSettings(@RequestBody Map<String, Integer> body) {
|
|
||||||
Integer v = body != null ? body.get("maxParticipantsBeforeBotJoin") : null;
|
|
||||||
if (v == null) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "maxParticipantsBeforeBotJoin is required"));
|
|
||||||
}
|
|
||||||
int updated = configurationService.setMaxParticipantsBeforeBotJoin(v);
|
|
||||||
return ResponseEntity.ok(Map.of("maxParticipantsBeforeBotJoin", updated));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.lottery.lottery.controller;
|
|
||||||
|
|
||||||
import com.lottery.lottery.dto.AdminConfigurationsRequest;
|
|
||||||
import com.lottery.lottery.service.BotConfigService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Admin API for safe bots and flexible bots (winner-override config used e.g. with /remotebet).
|
|
||||||
* Configurations tab in admin panel uses GET/PUT /api/admin/configurations.
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/admin/configurations")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
|
||||||
public class AdminConfigurationsController {
|
|
||||||
|
|
||||||
private final BotConfigService botConfigService;
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
public ResponseEntity<BotConfigService.BotConfigDto> getConfig() {
|
|
||||||
return ResponseEntity.ok(botConfigService.getConfig());
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping
|
|
||||||
public ResponseEntity<BotConfigService.BotConfigDto> updateConfig(
|
|
||||||
@RequestBody AdminConfigurationsRequest request
|
|
||||||
) {
|
|
||||||
List<Integer> safeIds = request.getSafeBotUserIds() != null
|
|
||||||
? request.getSafeBotUserIds()
|
|
||||||
: Collections.emptyList();
|
|
||||||
List<BotConfigService.FlexibleBotEntryDto> flexibleBots = Collections.emptyList();
|
|
||||||
if (request.getFlexibleBots() != null) {
|
|
||||||
flexibleBots = request.getFlexibleBots().stream()
|
|
||||||
.filter(e -> e != null && e.getUserId() != null && e.getWinRate() != null)
|
|
||||||
.map(e -> new BotConfigService.FlexibleBotEntryDto(e.getUserId(), e.getWinRate()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
botConfigService.setSafeBotUserIds(safeIds);
|
|
||||||
botConfigService.setFlexibleBots(flexibleBots);
|
|
||||||
return ResponseEntity.ok(botConfigService.getConfig());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import com.lottery.lottery.model.Payment;
|
|||||||
import com.lottery.lottery.model.Payout;
|
import com.lottery.lottery.model.Payout;
|
||||||
import com.lottery.lottery.model.SupportTicket;
|
import com.lottery.lottery.model.SupportTicket;
|
||||||
import com.lottery.lottery.model.UserA;
|
import com.lottery.lottery.model.UserA;
|
||||||
import com.lottery.lottery.repository.GameRoundRepository;
|
|
||||||
import com.lottery.lottery.repository.PaymentRepository;
|
import com.lottery.lottery.repository.PaymentRepository;
|
||||||
import com.lottery.lottery.repository.PayoutRepository;
|
import com.lottery.lottery.repository.PayoutRepository;
|
||||||
import com.lottery.lottery.repository.SupportTicketRepository;
|
import com.lottery.lottery.repository.SupportTicketRepository;
|
||||||
@@ -31,7 +30,6 @@ public class AdminDashboardController {
|
|||||||
private final UserARepository userARepository;
|
private final UserARepository userARepository;
|
||||||
private final PaymentRepository paymentRepository;
|
private final PaymentRepository paymentRepository;
|
||||||
private final PayoutRepository payoutRepository;
|
private final PayoutRepository payoutRepository;
|
||||||
private final GameRoundRepository gameRoundRepository;
|
|
||||||
private final SupportTicketRepository supportTicketRepository;
|
private final SupportTicketRepository supportTicketRepository;
|
||||||
|
|
||||||
@GetMapping("/stats")
|
@GetMapping("/stats")
|
||||||
@@ -105,17 +103,6 @@ public class AdminDashboardController {
|
|||||||
BigDecimal cryptoNetRevenueWeek = cryptoRevenueWeek.subtract(cryptoPayoutsWeek);
|
BigDecimal cryptoNetRevenueWeek = cryptoRevenueWeek.subtract(cryptoPayoutsWeek);
|
||||||
BigDecimal cryptoNetRevenueMonth = cryptoRevenueMonth.subtract(cryptoPayoutsMonth);
|
BigDecimal cryptoNetRevenueMonth = cryptoRevenueMonth.subtract(cryptoPayoutsMonth);
|
||||||
|
|
||||||
// Game Rounds
|
|
||||||
long totalRounds = gameRoundRepository.count();
|
|
||||||
long roundsToday = gameRoundRepository.countByResolvedAtAfter(todayStart);
|
|
||||||
long roundsWeek = gameRoundRepository.countByResolvedAtAfter(weekStart);
|
|
||||||
long roundsMonth = gameRoundRepository.countByResolvedAtAfter(monthStart);
|
|
||||||
|
|
||||||
// Average Round Pool (from resolved rounds) - round to int
|
|
||||||
Double avgPoolDouble = gameRoundRepository.avgTotalBetByResolvedAtAfter(monthStart)
|
|
||||||
.orElse(0.0);
|
|
||||||
int avgPool = (int) Math.round(avgPoolDouble);
|
|
||||||
|
|
||||||
// Support Tickets
|
// Support Tickets
|
||||||
long openTickets = supportTicketRepository.countByStatus(SupportTicket.TicketStatus.OPENED);
|
long openTickets = supportTicketRepository.countByStatus(SupportTicket.TicketStatus.OPENED);
|
||||||
// Count tickets closed today
|
// Count tickets closed today
|
||||||
@@ -176,11 +163,11 @@ public class AdminDashboardController {
|
|||||||
stats.put("crypto", crypto);
|
stats.put("crypto", crypto);
|
||||||
|
|
||||||
stats.put("rounds", Map.of(
|
stats.put("rounds", Map.of(
|
||||||
"total", totalRounds,
|
"total", 0L,
|
||||||
"today", roundsToday,
|
"today", 0L,
|
||||||
"week", roundsWeek,
|
"week", 0L,
|
||||||
"month", roundsMonth,
|
"month", 0L,
|
||||||
"avgPool", avgPool
|
"avgPool", 0
|
||||||
));
|
));
|
||||||
|
|
||||||
stats.put("supportTickets", Map.of(
|
stats.put("supportTickets", Map.of(
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
package com.lottery.lottery.controller;
|
|
||||||
|
|
||||||
import com.lottery.lottery.dto.AdminRoomDetailDto;
|
|
||||||
import com.lottery.lottery.dto.AdminRoomOnlineUserDto;
|
|
||||||
import com.lottery.lottery.dto.AdminRoomSummaryDto;
|
|
||||||
import com.lottery.lottery.service.GameRoomService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/admin/rooms")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AdminRoomController {
|
|
||||||
|
|
||||||
private final GameRoomService gameRoomService;
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
|
||||||
public ResponseEntity<List<AdminRoomSummaryDto>> listRooms() {
|
|
||||||
return ResponseEntity.ok(gameRoomService.getAdminRoomSummaries());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/online-users")
|
|
||||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
|
||||||
public ResponseEntity<List<AdminRoomOnlineUserDto>> getOnlineUsers() {
|
|
||||||
return ResponseEntity.ok(gameRoomService.getAdminOnlineUsersAcrossRooms());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{roomNumber}")
|
|
||||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
|
||||||
public ResponseEntity<AdminRoomDetailDto> getRoomDetail(@PathVariable Integer roomNumber) {
|
|
||||||
if (roomNumber == null || roomNumber < 1 || roomNumber > 3) {
|
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(gameRoomService.getAdminRoomDetail(roomNumber));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{roomNumber}/repair")
|
|
||||||
@PreAuthorize("hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<Map<String, Object>> repairRoom(@PathVariable Integer roomNumber) {
|
|
||||||
if (roomNumber == null || roomNumber < 1 || roomNumber > 3) {
|
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
gameRoomService.repairRoom(roomNumber);
|
|
||||||
return ResponseEntity.ok(Map.of("success", true, "message", "Repair completed for room " + roomNumber));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ResponseEntity.internalServerError()
|
|
||||||
.body(Map.of("success", false, "message", e.getMessage() != null ? e.getMessage() : "Repair failed"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ public class AdminUserController {
|
|||||||
private static final Set<String> SORTABLE_FIELDS = Set.of(
|
private static final Set<String> SORTABLE_FIELDS = Set.of(
|
||||||
"id", "screenName", "telegramId", "telegramName", "isPremium",
|
"id", "screenName", "telegramId", "telegramName", "isPremium",
|
||||||
"languageCode", "countryCode", "deviceCode", "dateReg", "dateLogin", "banned",
|
"languageCode", "countryCode", "deviceCode", "dateReg", "dateLogin", "banned",
|
||||||
"balanceA", "depositTotal", "withdrawTotal", "roundsPlayed", "referralCount", "profit"
|
"balanceA", "depositTotal", "withdrawTotal", "referralCount", "profit"
|
||||||
);
|
);
|
||||||
private static final Set<String> DEPOSIT_SORT_FIELDS = Set.of("id", "usdAmount", "status", "orderId", "createdAt", "completedAt");
|
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");
|
private static final Set<String> WITHDRAWAL_SORT_FIELDS = Set.of("id", "usdAmount", "cryptoName", "amountToSend", "txhash", "status", "paymentId", "createdAt", "resolvedAt");
|
||||||
@@ -57,17 +57,14 @@ public class AdminUserController {
|
|||||||
@RequestParam(required = false) Integer dateRegTo,
|
@RequestParam(required = false) Integer dateRegTo,
|
||||||
@RequestParam(required = false) Long balanceMin,
|
@RequestParam(required = false) Long balanceMin,
|
||||||
@RequestParam(required = false) Long balanceMax,
|
@RequestParam(required = false) Long balanceMax,
|
||||||
@RequestParam(required = false) Integer roundsPlayedMin,
|
|
||||||
@RequestParam(required = false) Integer roundsPlayedMax,
|
|
||||||
@RequestParam(required = false) Integer referralCountMin,
|
@RequestParam(required = false) Integer referralCountMin,
|
||||||
@RequestParam(required = false) Integer referralCountMax,
|
@RequestParam(required = false) Integer referralCountMax,
|
||||||
@RequestParam(required = false) Integer referrerId,
|
@RequestParam(required = false) Integer referrerId,
|
||||||
@RequestParam(required = false) Integer referralLevel,
|
@RequestParam(required = false) Integer referralLevel,
|
||||||
@RequestParam(required = false) String ip) {
|
@RequestParam(required = false) String ip) {
|
||||||
|
|
||||||
// Build sort. Fields on UserB/UserD (balanceA, depositTotal, withdrawTotal, roundsPlayed, referralCount)
|
// Build sort. Fields on UserB/UserD (balanceA, depositTotal, withdrawTotal, referralCount) are handled in service via custom query.
|
||||||
// are handled in service via custom query; others are applied to UserA.
|
Set<String> sortRequiresJoin = Set.of("balanceA", "depositTotal", "withdrawTotal", "referralCount", "profit");
|
||||||
Set<String> sortRequiresJoin = Set.of("balanceA", "depositTotal", "withdrawTotal", "roundsPlayed", "referralCount", "profit");
|
|
||||||
String effectiveSortBy = sortBy != null && sortBy.trim().isEmpty() ? null : (sortBy != null ? sortBy.trim() : null);
|
String effectiveSortBy = sortBy != null && sortBy.trim().isEmpty() ? null : (sortBy != null ? sortBy.trim() : null);
|
||||||
if (effectiveSortBy != null && sortRequiresJoin.contains(effectiveSortBy)) {
|
if (effectiveSortBy != null && sortRequiresJoin.contains(effectiveSortBy)) {
|
||||||
// Pass through; service will use custom ordered query
|
// Pass through; service will use custom ordered query
|
||||||
@@ -96,8 +93,6 @@ public class AdminUserController {
|
|||||||
dateRegTo,
|
dateRegTo,
|
||||||
balanceMinBigint,
|
balanceMinBigint,
|
||||||
balanceMaxBigint,
|
balanceMaxBigint,
|
||||||
roundsPlayedMin,
|
|
||||||
roundsPlayedMax,
|
|
||||||
referralCountMin,
|
referralCountMin,
|
||||||
referralCountMax,
|
referralCountMax,
|
||||||
referrerId,
|
referrerId,
|
||||||
@@ -152,28 +147,6 @@ public class AdminUserController {
|
|||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/game-rounds")
|
|
||||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
|
||||||
public ResponseEntity<Map<String, Object>> getUserGameRounds(
|
|
||||||
@PathVariable Integer id,
|
|
||||||
@RequestParam(defaultValue = "0") int page,
|
|
||||||
@RequestParam(defaultValue = "50") int size) {
|
|
||||||
|
|
||||||
Pageable pageable = PageRequest.of(page, size);
|
|
||||||
Page<AdminGameRoundDto> rounds = adminUserService.getUserGameRounds(id, pageable);
|
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("content", rounds.getContent());
|
|
||||||
response.put("totalElements", rounds.getTotalElements());
|
|
||||||
response.put("totalPages", rounds.getTotalPages());
|
|
||||||
response.put("currentPage", rounds.getNumber());
|
|
||||||
response.put("size", rounds.getSize());
|
|
||||||
response.put("hasNext", rounds.hasNext());
|
|
||||||
response.put("hasPrevious", rounds.hasPrevious());
|
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}/payments")
|
@GetMapping("/{id}/payments")
|
||||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
||||||
public ResponseEntity<Map<String, Object>> getUserPayments(
|
public ResponseEntity<Map<String, Object>> getUserPayments(
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
package com.lottery.lottery.controller;
|
|
||||||
|
|
||||||
import com.lottery.lottery.dto.CompletedRoundDto;
|
|
||||||
import com.lottery.lottery.dto.GameHistoryEntryDto;
|
|
||||||
import com.lottery.lottery.model.GameRound;
|
|
||||||
import com.lottery.lottery.repository.GameRoundRepository;
|
|
||||||
import com.lottery.lottery.security.UserContext;
|
|
||||||
import com.lottery.lottery.service.AvatarService;
|
|
||||||
import com.lottery.lottery.service.GameHistoryService;
|
|
||||||
import com.lottery.lottery.repository.UserARepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/game")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class GameController {
|
|
||||||
|
|
||||||
private final GameRoundRepository gameRoundRepository;
|
|
||||||
private final UserARepository userARepository;
|
|
||||||
private final AvatarService avatarService;
|
|
||||||
private final GameHistoryService gameHistoryService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the last 10 completed rounds for a specific room.
|
|
||||||
* Fetches data from game_rounds table only.
|
|
||||||
*/
|
|
||||||
@GetMapping("/room/{roomNumber}/completed-rounds")
|
|
||||||
public ResponseEntity<List<CompletedRoundDto>> getCompletedRounds(
|
|
||||||
@PathVariable Integer roomNumber
|
|
||||||
) {
|
|
||||||
List<GameRound> rounds = gameRoundRepository.findLastCompletedRoundsByRoomNumber(
|
|
||||||
roomNumber,
|
|
||||||
PageRequest.of(0, 10)
|
|
||||||
);
|
|
||||||
|
|
||||||
List<CompletedRoundDto> completedRounds = rounds.stream()
|
|
||||||
.map(round -> {
|
|
||||||
// Calculate winner's chance from game_rounds table data
|
|
||||||
Double winChance = null;
|
|
||||||
if (round.getWinnerBet() != null && round.getTotalBet() != null && round.getTotalBet() > 0) {
|
|
||||||
winChance = ((double) round.getWinnerBet() / round.getTotalBet()) * 100.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get winner's screen name and avatar
|
|
||||||
String screenName = null;
|
|
||||||
String avatarUrl = null;
|
|
||||||
if (round.getWinnerUserId() != null) {
|
|
||||||
screenName = userARepository.findById(round.getWinnerUserId())
|
|
||||||
.map(userA -> userA.getScreenName())
|
|
||||||
.orElse(null);
|
|
||||||
avatarUrl = avatarService.getAvatarUrl(round.getWinnerUserId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return CompletedRoundDto.builder()
|
|
||||||
.roundId(round.getId())
|
|
||||||
.winnerUserId(round.getWinnerUserId())
|
|
||||||
.winnerScreenName(screenName)
|
|
||||||
.winnerAvatarUrl(avatarUrl)
|
|
||||||
.winnerBet(round.getWinnerBet())
|
|
||||||
.payout(round.getPayout())
|
|
||||||
.totalBet(round.getTotalBet())
|
|
||||||
.winChance(winChance)
|
|
||||||
.resolvedAt(round.getResolvedAt() != null ? round.getResolvedAt().toEpochMilli() : null)
|
|
||||||
.build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return ResponseEntity.ok(completedRounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets WIN transactions for the current user from the last 30 days with pagination.
|
|
||||||
*
|
|
||||||
* @param page Page number (0-indexed, default 0)
|
|
||||||
* @param timezone Optional timezone (e.g., "Europe/London"). If not provided, uses UTC.
|
|
||||||
*/
|
|
||||||
@GetMapping("/history")
|
|
||||||
public ResponseEntity<org.springframework.data.domain.Page<GameHistoryEntryDto>> getUserGameHistory(
|
|
||||||
@RequestParam(defaultValue = "0") int page,
|
|
||||||
@RequestParam(required = false) String timezone) {
|
|
||||||
Integer userId = UserContext.get().getId();
|
|
||||||
com.lottery.lottery.model.UserA user = UserContext.get();
|
|
||||||
String languageCode = user.getLanguageCode();
|
|
||||||
if (languageCode == null || languageCode.isEmpty() || "XX".equals(languageCode)) {
|
|
||||||
languageCode = "EN";
|
|
||||||
}
|
|
||||||
org.springframework.data.domain.Page<GameHistoryEntryDto> history = gameHistoryService.getUserGameHistory(userId, page, timezone, languageCode);
|
|
||||||
return ResponseEntity.ok(history);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
package com.lottery.lottery.controller;
|
|
||||||
|
|
||||||
import com.lottery.lottery.config.WebSocketAuthInterceptor;
|
|
||||||
import com.lottery.lottery.dto.BalanceUpdateDto;
|
|
||||||
import com.lottery.lottery.dto.GameRoomStateDto;
|
|
||||||
import com.lottery.lottery.dto.JoinRoundRequest;
|
|
||||||
import com.lottery.lottery.exception.GameException;
|
|
||||||
import com.lottery.lottery.service.GameRoomService;
|
|
||||||
import com.lottery.lottery.service.LocalizationService;
|
|
||||||
import com.lottery.lottery.service.UserService;
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import jakarta.validation.ConstraintViolation;
|
|
||||||
import jakarta.validation.ConstraintViolationException;
|
|
||||||
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
|
|
||||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
|
||||||
import org.springframework.messaging.handler.annotation.Payload;
|
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
|
||||||
import org.springframework.messaging.simp.annotation.SubscribeMapping;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Controller
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class GameWebSocketController {
|
|
||||||
|
|
||||||
private final GameRoomService gameRoomService;
|
|
||||||
private final SimpMessagingTemplate messagingTemplate;
|
|
||||||
private final UserService userService;
|
|
||||||
private final LocalizationService localizationService;
|
|
||||||
|
|
||||||
// Track which users are subscribed to which rooms
|
|
||||||
private final Map<Integer, Integer> userRoomSubscriptions = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
// Track winners who have already received balance updates (to avoid duplicates)
|
|
||||||
private final Map<Integer, Integer> notifiedWinners = new ConcurrentHashMap<>(); // roomNumber -> winnerUserId
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the controller and sets up balance update callback.
|
|
||||||
*/
|
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
// Set callback for balance update notifications
|
|
||||||
gameRoomService.setBalanceUpdateCallback(this::notifyBalanceUpdate);
|
|
||||||
|
|
||||||
// Set callback for state broadcast notifications (event-driven)
|
|
||||||
gameRoomService.setStateBroadcastCallback(this::broadcastRoomState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies a user about balance update.
|
|
||||||
* Called by GameRoomService for single participant refunds (no spin, so immediate update is fine).
|
|
||||||
*/
|
|
||||||
private void notifyBalanceUpdate(Integer userId) {
|
|
||||||
String username = String.valueOf(userId);
|
|
||||||
sendBalanceUpdate(username, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles join round request from client.
|
|
||||||
*/
|
|
||||||
@MessageMapping("/game/join")
|
|
||||||
public void joinRound(@Valid @Payload JoinRoundRequest request, WebSocketAuthInterceptor.StompPrincipal principal) {
|
|
||||||
Integer userId = principal.getUserId();
|
|
||||||
|
|
||||||
// Additional validation beyond @Valid annotations
|
|
||||||
// @Valid handles null checks and basic constraints, but we add explicit checks for clarity
|
|
||||||
if (request == null) {
|
|
||||||
throw new GameException(localizationService.getMessage("game.error.invalidRequest"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate room number range (1-3)
|
|
||||||
// This is also covered by @Min/@Max, but explicit check provides better error message
|
|
||||||
if (request.getRoomNumber() == null || request.getRoomNumber() < 1 || request.getRoomNumber() > 3) {
|
|
||||||
throw new GameException(localizationService.getMessage("game.error.roomNumberInvalid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate bet amount is positive (also covered by @Positive, but explicit for clarity)
|
|
||||||
if (request.getBetAmount() == null || request.getBetAmount() <= 0) {
|
|
||||||
throw new GameException(localizationService.getMessage("game.error.betMustBePositive"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Join the round
|
|
||||||
GameRoomStateDto state = gameRoomService.joinRound(userId, request.getRoomNumber(), request.getBetAmount());
|
|
||||||
|
|
||||||
// Track subscription
|
|
||||||
userRoomSubscriptions.put(userId, request.getRoomNumber());
|
|
||||||
|
|
||||||
// Send balance update to the user who joined
|
|
||||||
sendBalanceUpdate(principal.getName(), userId);
|
|
||||||
|
|
||||||
// State is already broadcast by GameRoomService.joinRound() via callback (event-driven)
|
|
||||||
// No need to broadcast again here
|
|
||||||
|
|
||||||
} catch (GameException e) {
|
|
||||||
// User-friendly error message
|
|
||||||
sendErrorToUser(principal.getName(), e.getUserMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Generic error - don't expose technical details
|
|
||||||
log.error("Unexpected error joining round for user {}", userId, e);
|
|
||||||
sendErrorToUser(principal.getName(), localizationService.getMessage("common.error.unknown"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends error message to user.
|
|
||||||
*/
|
|
||||||
private void sendErrorToUser(String username, String errorMessage) {
|
|
||||||
messagingTemplate.convertAndSendToUser(
|
|
||||||
username,
|
|
||||||
"/queue/errors",
|
|
||||||
Map.of("error", errorMessage)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global exception handler for WebSocket messages.
|
|
||||||
*/
|
|
||||||
@MessageExceptionHandler
|
|
||||||
public void handleException(Exception ex, WebSocketAuthInterceptor.StompPrincipal principal) {
|
|
||||||
String userMessage;
|
|
||||||
|
|
||||||
if (ex instanceof GameException) {
|
|
||||||
userMessage = ((GameException) ex).getUserMessage();
|
|
||||||
} else if (ex instanceof ConstraintViolationException) {
|
|
||||||
// Handle validation errors from @Valid annotation
|
|
||||||
ConstraintViolationException cve = (ConstraintViolationException) ex;
|
|
||||||
userMessage = cve.getConstraintViolations().stream()
|
|
||||||
.map(ConstraintViolation::getMessage)
|
|
||||||
.findFirst()
|
|
||||||
.orElse("Validation failed. Please check your input.");
|
|
||||||
log.warn("Validation error for user {}: {}", principal.getUserId(), userMessage);
|
|
||||||
} else {
|
|
||||||
log.error("Unexpected WebSocket error", ex);
|
|
||||||
userMessage = localizationService.getMessage("common.error.unknown");
|
|
||||||
}
|
|
||||||
|
|
||||||
sendErrorToUser(principal.getName(), userMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends current room state when client subscribes.
|
|
||||||
* Note: SubscribeMapping doesn't support path variables well, so we'll handle subscription in joinRound
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts room state to all subscribers.
|
|
||||||
* Called by GameRoomService via callback (event-driven).
|
|
||||||
*/
|
|
||||||
public void broadcastRoomState(Integer roomNumber, GameRoomStateDto state) {
|
|
||||||
messagingTemplate.convertAndSend("/topic/room/" + roomNumber, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends balance update to a specific user.
|
|
||||||
*/
|
|
||||||
private void sendBalanceUpdate(String username, Integer userId) {
|
|
||||||
try {
|
|
||||||
// Get current balance from database
|
|
||||||
Long balance = userService.getUserBalance(userId);
|
|
||||||
if (balance != null) {
|
|
||||||
BalanceUpdateDto balanceUpdate = BalanceUpdateDto.builder()
|
|
||||||
.balanceA(balance)
|
|
||||||
.build();
|
|
||||||
messagingTemplate.convertAndSendToUser(
|
|
||||||
username,
|
|
||||||
"/queue/balance",
|
|
||||||
balanceUpdate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to send balance update to user {}", userId, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
package com.lottery.lottery.controller;
|
|
||||||
|
|
||||||
import com.lottery.lottery.exception.GameException;
|
|
||||||
import com.lottery.lottery.service.FeatureSwitchService;
|
|
||||||
import com.lottery.lottery.service.GameRoomService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unauthenticated API for 3rd party to register a user into a round (remote bet).
|
|
||||||
* Protected by a shared token in the path and a runtime feature switch.
|
|
||||||
* Same business logic as in-app join (balance, commissions, transactions, visibility in app).
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/remotebet")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class RemoteBetController {
|
|
||||||
|
|
||||||
private static final long TICKETS_TO_BIGINT = 1_000_000L;
|
|
||||||
|
|
||||||
@Value("${app.remote-bet.token:}")
|
|
||||||
private String configuredToken;
|
|
||||||
|
|
||||||
private final FeatureSwitchService featureSwitchService;
|
|
||||||
private final GameRoomService gameRoomService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the user to the current round in the given room with the given bet.
|
|
||||||
* GET /api/remotebet/{token}?user_id=228&room=2&amount=5&unique=false
|
|
||||||
* Or with random range: at least one of rand_min or rand_max (amount ignored).
|
|
||||||
* - user_id: db_users_a.id
|
|
||||||
* - room: room number (1, 2, or 3)
|
|
||||||
* - amount: bet in tickets. Ignored when rand_min and/or rand_max are provided.
|
|
||||||
* - unique: optional. If true, user can only have one bet per room per round (repeated calls no-op).
|
|
||||||
* - rand_min: optional. If only rand_min: random between rand_min and room max. If both: random between rand_min and rand_max.
|
|
||||||
* - rand_max: optional. If only rand_max: random between room min and rand_max. If both: random between rand_min and rand_max.
|
|
||||||
* Params are validated against room min/max (rand_min >= room min, rand_max <= room max; when both, rand_min <= rand_max).
|
|
||||||
*/
|
|
||||||
@GetMapping("/{token}")
|
|
||||||
public ResponseEntity<?> remoteBet(
|
|
||||||
@PathVariable String token,
|
|
||||||
@RequestParam(name = "user_id") Integer userId,
|
|
||||||
@RequestParam(name = "room") Integer room,
|
|
||||||
@RequestParam(name = "amount") Integer amountTickets,
|
|
||||||
@RequestParam(name = "unique", required = false) Boolean unique,
|
|
||||||
@RequestParam(name = "rand_min", required = false) Integer randMin,
|
|
||||||
@RequestParam(name = "rand_max", required = false) Integer randMax) {
|
|
||||||
|
|
||||||
if (configuredToken == null || configuredToken.isEmpty() || !configuredToken.equals(token)) {
|
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
|
||||||
}
|
|
||||||
if (!featureSwitchService.isRemoteBetEnabled()) {
|
|
||||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean useRandomRange = randMin != null || randMax != null;
|
|
||||||
long betAmount;
|
|
||||||
int amountTicketsForLog;
|
|
||||||
|
|
||||||
if (useRandomRange) {
|
|
||||||
GameRoomService.BetLimits limits = GameRoomService.getBetLimitsForRoom(room);
|
|
||||||
long roomMinTickets = limits.minBet() / TICKETS_TO_BIGINT;
|
|
||||||
long roomMaxTickets = limits.maxBet() / TICKETS_TO_BIGINT;
|
|
||||||
|
|
||||||
long effectiveMinTickets;
|
|
||||||
long effectiveMaxTickets;
|
|
||||||
|
|
||||||
if (randMin != null && randMax != null) {
|
|
||||||
if (randMin < roomMinTickets) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"rand_min must not be lower than room min bet (" + roomMinTickets + " tickets)"));
|
|
||||||
}
|
|
||||||
if (randMax > roomMaxTickets) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"rand_max must not be higher than room max bet (" + roomMaxTickets + " tickets)"));
|
|
||||||
}
|
|
||||||
if (randMin > randMax) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"rand_min must be less than or equal to rand_max"));
|
|
||||||
}
|
|
||||||
effectiveMinTickets = randMin;
|
|
||||||
effectiveMaxTickets = randMax;
|
|
||||||
} else if (randMin != null) {
|
|
||||||
if (randMin < roomMinTickets) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"rand_min must not be lower than room min bet (" + roomMinTickets + " tickets)"));
|
|
||||||
}
|
|
||||||
if (randMin > roomMaxTickets) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"rand_min must not be higher than room max bet (" + roomMaxTickets + " tickets)"));
|
|
||||||
}
|
|
||||||
effectiveMinTickets = randMin;
|
|
||||||
effectiveMaxTickets = roomMaxTickets;
|
|
||||||
} else {
|
|
||||||
if (randMax < roomMinTickets) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"rand_max must not be lower than room min bet (" + roomMinTickets + " tickets)"));
|
|
||||||
}
|
|
||||||
if (randMax > roomMaxTickets) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"rand_max must not be higher than room max bet (" + roomMaxTickets + " tickets)"));
|
|
||||||
}
|
|
||||||
effectiveMinTickets = roomMinTickets;
|
|
||||||
effectiveMaxTickets = randMax;
|
|
||||||
}
|
|
||||||
|
|
||||||
long currentUserBetBigint = gameRoomService.getCurrentUserBetInRoom(userId, room);
|
|
||||||
long maxAdditionalBigint = Math.max(0L, limits.maxBet() - currentUserBetBigint);
|
|
||||||
long maxAdditionalTickets = maxAdditionalBigint / TICKETS_TO_BIGINT;
|
|
||||||
if (maxAdditionalTickets < limits.minBet() / TICKETS_TO_BIGINT) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"Max bet for this room already reached"));
|
|
||||||
}
|
|
||||||
|
|
||||||
effectiveMaxTickets = Math.min(effectiveMaxTickets, maxAdditionalTickets);
|
|
||||||
if (effectiveMinTickets > effectiveMaxTickets) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"Random range exceeds remaining bet capacity for this room"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Room 1: any integer; Room 2: divisible by 10; Room 3: divisible by 100
|
|
||||||
long step = room == 2 ? 10L : (room == 3 ? 100L : 1L);
|
|
||||||
long minAligned = roundUpToMultiple(effectiveMinTickets, step);
|
|
||||||
long maxAligned = roundDownToMultiple(effectiveMaxTickets, step);
|
|
||||||
if (minAligned > maxAligned) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, 0,
|
|
||||||
"No valid random value in range for room " + room + " (room 2 must be multiple of 10, room 3 multiple of 100)"));
|
|
||||||
}
|
|
||||||
long randomTickets = minAligned >= maxAligned
|
|
||||||
? minAligned
|
|
||||||
: minAligned + step * ThreadLocalRandom.current().nextLong(0, (maxAligned - minAligned) / step + 1);
|
|
||||||
betAmount = randomTickets * TICKETS_TO_BIGINT;
|
|
||||||
amountTicketsForLog = (int) randomTickets;
|
|
||||||
} else {
|
|
||||||
betAmount = (long) amountTickets * TICKETS_TO_BIGINT;
|
|
||||||
amountTicketsForLog = amountTickets;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean uniqueBet = Boolean.TRUE.equals(unique);
|
|
||||||
try {
|
|
||||||
var result = gameRoomService.joinRoundWithResult(userId, room, betAmount, uniqueBet);
|
|
||||||
var state = result.getState();
|
|
||||||
Long roundId = state.getRoundId();
|
|
||||||
int betTicketsForResponse = result.getBetTicketsForResponse();
|
|
||||||
String randRangeLog = useRandomRange ? (randMin != null ? randMin : "roomMin") + "-" + (randMax != null ? randMax : "roomMax") : "no";
|
|
||||||
log.info("Remote bet: user connected to round remotely, userId={}, roundId={}, roomId={}, betTickets={}, unique={}, randRange={}",
|
|
||||||
userId, roundId, room, betTicketsForResponse, uniqueBet, randRangeLog);
|
|
||||||
return ResponseEntity.ok(new RemoteBetResponse(true, roundId != null ? roundId.intValue() : null, room, betTicketsForResponse));
|
|
||||||
} catch (GameException e) {
|
|
||||||
return ResponseEntity.badRequest().body(new RemoteBetResponse(false, null, room, amountTicketsForLog, e.getUserMessage()));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Remote bet failed for userId={}, room={}, amount={}", userId, room, amountTicketsForLog, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
.body(new RemoteBetResponse(false, null, room, amountTicketsForLog, "Internal error"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
@lombok.AllArgsConstructor
|
|
||||||
@lombok.NoArgsConstructor
|
|
||||||
public static class RemoteBetResponse {
|
|
||||||
private boolean success;
|
|
||||||
private Integer roundId;
|
|
||||||
private Integer room;
|
|
||||||
private Integer betTickets;
|
|
||||||
private String error;
|
|
||||||
|
|
||||||
public RemoteBetResponse(boolean success, Integer roundId, Integer room, Integer betTickets) {
|
|
||||||
this(success, roundId, room, betTickets, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Round value up to next multiple of step (e.g. 23, 10 -> 30). */
|
|
||||||
private static long roundUpToMultiple(long value, long step) {
|
|
||||||
if (step <= 0) return value;
|
|
||||||
return ((value + step - 1) / step) * step;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Round value down to previous multiple of step (e.g. 197, 10 -> 190). */
|
|
||||||
private static long roundDownToMultiple(long value, long step) {
|
|
||||||
if (step <= 0) return value;
|
|
||||||
return (value / step) * step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -70,9 +70,6 @@ public class UserCheckController {
|
|||||||
// Convert to tickets (balance_a / 1,000,000)
|
// Convert to tickets (balance_a / 1,000,000)
|
||||||
Double tickets = balanceA / 1_000_000.0;
|
Double tickets = balanceA / 1_000_000.0;
|
||||||
|
|
||||||
// Get rounds_played from db_users_b
|
|
||||||
Integer roundsPlayed = userBOpt.map(UserB::getRoundsPlayed).orElse(0);
|
|
||||||
|
|
||||||
// Get referer_id_1 from db_users_d
|
// Get referer_id_1 from db_users_d
|
||||||
Optional<UserD> userDOpt = userDRepository.findById(userId);
|
Optional<UserD> userDOpt = userDRepository.findById(userId);
|
||||||
Integer refererId = userDOpt.map(UserD::getRefererId1).orElse(0);
|
Integer refererId = userDOpt.map(UserD::getRefererId1).orElse(0);
|
||||||
@@ -91,7 +88,6 @@ public class UserCheckController {
|
|||||||
.tickets(tickets)
|
.tickets(tickets)
|
||||||
.depositTotal(depositTotal)
|
.depositTotal(depositTotal)
|
||||||
.refererId(refererId)
|
.refererId(refererId)
|
||||||
.roundsPlayed(roundsPlayed)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AdminBotConfigDto {
|
|
||||||
private Integer id;
|
|
||||||
private Integer userId;
|
|
||||||
/** User screen name from db_users_a (for display). */
|
|
||||||
private String screenName;
|
|
||||||
private Boolean room1;
|
|
||||||
private Boolean room2;
|
|
||||||
private Boolean room3;
|
|
||||||
/** Time window start UTC, format HH:mm (e.g. "14:00"). */
|
|
||||||
private String timeUtcStart;
|
|
||||||
/** Time window end UTC, format HH:mm (e.g. "17:00"). */
|
|
||||||
private String timeUtcEnd;
|
|
||||||
/** Min bet in bigint (1 ticket = 1_000_000). */
|
|
||||||
private Long betMin;
|
|
||||||
/** Max bet in bigint. */
|
|
||||||
private Long betMax;
|
|
||||||
private String persona;
|
|
||||||
private Boolean active;
|
|
||||||
private Instant createdAt;
|
|
||||||
private Instant updatedAt;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AdminBotConfigRequest {
|
|
||||||
@NotNull(message = "userId is required")
|
|
||||||
private Integer userId;
|
|
||||||
@NotNull
|
|
||||||
private Boolean room1;
|
|
||||||
@NotNull
|
|
||||||
private Boolean room2;
|
|
||||||
@NotNull
|
|
||||||
private Boolean room3;
|
|
||||||
/** Time window start UTC, format HH:mm (e.g. "14:00"). */
|
|
||||||
@NotNull
|
|
||||||
private String timeUtcStart;
|
|
||||||
/** Time window end UTC, format HH:mm (e.g. "17:00"). */
|
|
||||||
@NotNull
|
|
||||||
private String timeUtcEnd;
|
|
||||||
/** Min bet in bigint (1 ticket = 1_000_000). */
|
|
||||||
@NotNull
|
|
||||||
private Long betMin;
|
|
||||||
/** Max bet in bigint. */
|
|
||||||
@NotNull
|
|
||||||
private Long betMax;
|
|
||||||
private String persona; // conservative, aggressive, balanced; default balanced
|
|
||||||
@NotNull
|
|
||||||
private Boolean active;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class AdminConfigurationsRequest {
|
|
||||||
|
|
||||||
private List<Integer> safeBotUserIds = new ArrayList<>();
|
|
||||||
private List<FlexibleBotEntry> flexibleBots = new ArrayList<>();
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class FlexibleBotEntry {
|
|
||||||
private Integer userId;
|
|
||||||
private Double winRate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AdminGameRoundDto {
|
|
||||||
private Long roundId;
|
|
||||||
private Integer roomNumber;
|
|
||||||
private String phase;
|
|
||||||
private Long totalBet;
|
|
||||||
private Long userBet;
|
|
||||||
private Integer winnerUserId;
|
|
||||||
private Long winnerBet;
|
|
||||||
private Long payout;
|
|
||||||
private Long commission;
|
|
||||||
private Instant startedAt;
|
|
||||||
private Instant resolvedAt;
|
|
||||||
private Boolean isWinner;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AdminRoomDetailDto {
|
|
||||||
private Integer roomNumber;
|
|
||||||
private String phase;
|
|
||||||
private Long roundId;
|
|
||||||
private Long totalBetTickets;
|
|
||||||
private Double totalBetUsd;
|
|
||||||
private Integer registeredPlayers;
|
|
||||||
private Integer connectedUsers;
|
|
||||||
private List<AdminRoomParticipantDto> participants;
|
|
||||||
/** Viewers: same as participants section format but without tickets/chances (screen name + id). */
|
|
||||||
private List<AdminRoomViewerDto> connectedViewers;
|
|
||||||
private AdminRoomWinnerDto winner; // when phase is SPINNING or RESOLUTION
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One row for the "all online users across rooms" admin table.
|
|
||||||
* currentBetTickets null = viewer (not registered in current round).
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AdminRoomOnlineUserDto {
|
|
||||||
private Integer userId;
|
|
||||||
private String screenName;
|
|
||||||
private Integer roomNumber;
|
|
||||||
/** Current round bet in tickets; null if viewer (not registered). */
|
|
||||||
private Long currentBetTickets;
|
|
||||||
/** balance_a in bigint (divide by 1_000_000 for display). */
|
|
||||||
private Long balanceA;
|
|
||||||
private Long depositTotal;
|
|
||||||
private Integer depositCount;
|
|
||||||
private Long withdrawTotal;
|
|
||||||
private Integer withdrawCount;
|
|
||||||
private Integer roundsPlayed;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AdminRoomParticipantDto {
|
|
||||||
private Integer userId;
|
|
||||||
private String screenName;
|
|
||||||
private Long betTickets;
|
|
||||||
private Double chancePct;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AdminRoomSummaryDto {
|
|
||||||
private Integer roomNumber;
|
|
||||||
private String phase; // WAITING, COUNTDOWN, SPINNING, RESOLUTION
|
|
||||||
private Integer connectedUsers;
|
|
||||||
private Integer registeredPlayers;
|
|
||||||
private Long totalBetTickets;
|
|
||||||
private Double totalBetUsd; // tickets / 1000 for display
|
|
||||||
private Long roundId;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AdminRoomViewerDto {
|
|
||||||
private Integer userId;
|
|
||||||
private String screenName;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class AdminRoomWinnerDto {
|
|
||||||
private Integer userId;
|
|
||||||
private String screenName;
|
|
||||||
private Long betTickets;
|
|
||||||
private Double winChancePct;
|
|
||||||
}
|
|
||||||
@@ -41,9 +41,6 @@ public class AdminUserDetailDto {
|
|||||||
/** When true, the user cannot create any payout request. */
|
/** When true, the user cannot create any payout request. */
|
||||||
private Boolean withdrawalsDisabled;
|
private Boolean withdrawalsDisabled;
|
||||||
|
|
||||||
// Game Stats
|
|
||||||
private Integer roundsPlayed;
|
|
||||||
|
|
||||||
// Referral Info
|
// Referral Info
|
||||||
private Integer referralCount;
|
private Integer referralCount;
|
||||||
private Long totalCommissionsEarned;
|
private Long totalCommissionsEarned;
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ public class AdminUserDto {
|
|||||||
private Integer depositCount;
|
private Integer depositCount;
|
||||||
private Long withdrawTotal;
|
private Long withdrawTotal;
|
||||||
private Integer withdrawCount;
|
private Integer withdrawCount;
|
||||||
private Integer roundsPlayed;
|
|
||||||
private Integer dateReg;
|
private Integer dateReg;
|
||||||
private Integer dateLogin;
|
private Integer dateLogin;
|
||||||
private Integer banned;
|
private Integer banned;
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class CompletedRoundDto {
|
|
||||||
private Long roundId;
|
|
||||||
private Integer winnerUserId;
|
|
||||||
private String winnerScreenName;
|
|
||||||
private String winnerAvatarUrl;
|
|
||||||
private Long winnerBet;
|
|
||||||
private Long payout;
|
|
||||||
private Long totalBet;
|
|
||||||
private Double winChance; // winner's chance percentage
|
|
||||||
private Long resolvedAt; // timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class GameRoomStateDto {
|
|
||||||
@JsonProperty("rN")
|
|
||||||
private Integer roomNumber;
|
|
||||||
|
|
||||||
@JsonProperty("rI")
|
|
||||||
private Long roundId; // Current round id (null when no active round)
|
|
||||||
|
|
||||||
@JsonProperty("p")
|
|
||||||
private Integer phase; // 1=WAITING, 2=COUNTDOWN, 3=SPINNING, 4=RESOLUTION
|
|
||||||
|
|
||||||
@JsonProperty("tB")
|
|
||||||
private Long totalBet; // In tickets (not bigint)
|
|
||||||
|
|
||||||
@JsonProperty("rP")
|
|
||||||
private Integer registeredPlayers; // Users registered in current round
|
|
||||||
|
|
||||||
@JsonProperty("cU")
|
|
||||||
private Integer connectedUsers; // Total users connected to room (regardless of round participation)
|
|
||||||
|
|
||||||
@JsonProperty("aR")
|
|
||||||
private Map<Integer, Integer> allRoomsConnectedUsers; // Connected users count for all rooms (roomNumber -> count)
|
|
||||||
|
|
||||||
@JsonProperty("mB")
|
|
||||||
private Long minBet; // Minimum bet for this room (in tickets, not bigint)
|
|
||||||
|
|
||||||
@JsonProperty("mX")
|
|
||||||
private Long maxBet; // Maximum bet for this room (in tickets, not bigint)
|
|
||||||
|
|
||||||
@JsonProperty("cE")
|
|
||||||
private Instant countdownEndAt;
|
|
||||||
|
|
||||||
@JsonProperty("cR")
|
|
||||||
private Long countdownRemainingSeconds;
|
|
||||||
|
|
||||||
@JsonProperty("ps")
|
|
||||||
private List<ParticipantDto> participants;
|
|
||||||
|
|
||||||
@JsonProperty("w")
|
|
||||||
private WinnerDto winner;
|
|
||||||
|
|
||||||
@JsonProperty("sD")
|
|
||||||
private Long spinDuration; // milliseconds
|
|
||||||
|
|
||||||
@JsonProperty("sI")
|
|
||||||
private Long stopIndex; // for spin animation
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of joining a round (used by remote bet API to return state and bet amount for response).
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class JoinRoundResult {
|
|
||||||
private GameRoomStateDto state;
|
|
||||||
/** Bet tickets to report in API response (amount added this call, or current bet when unique=true no-op). */
|
|
||||||
private int betTicketsForResponse;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.lottery.lottery.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Incremental update DTO for room state changes.
|
|
||||||
* Used to send only what changed instead of full state.
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class RoomUpdateDto {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update type: USER_JOINED, USER_LEFT, FULL_STATE, PHASE_CHANGED
|
|
||||||
*/
|
|
||||||
private String type;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Participant data (for USER_JOINED)
|
|
||||||
*/
|
|
||||||
private ParticipantDto participant;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User ID (for USER_LEFT)
|
|
||||||
*/
|
|
||||||
private Integer userId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Full state (for FULL_STATE - fallback when incremental not possible)
|
|
||||||
*/
|
|
||||||
private GameRoomStateDto fullState;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Phase change data (for PHASE_CHANGED)
|
|
||||||
*/
|
|
||||||
private String phase;
|
|
||||||
private Long countdownRemaining;
|
|
||||||
private WinnerDto winner;
|
|
||||||
private Long stopIndex;
|
|
||||||
private Long spinDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -26,19 +26,14 @@ public class TransactionDto {
|
|||||||
private String date;
|
private String date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transaction type: DEPOSIT, WITHDRAWAL, WIN, BET, TASK_BONUS, DAILY_BONUS
|
* Transaction type: DEPOSIT, WITHDRAWAL, TASK_BONUS, CANCELLATION_OF_WITHDRAWAL
|
||||||
*/
|
*/
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task ID for TASK_BONUS type (null for DAILY_BONUS and other types)
|
* Task ID for TASK_BONUS type (null for other types)
|
||||||
*/
|
*/
|
||||||
private Integer taskId;
|
private Integer taskId;
|
||||||
|
|
||||||
/**
|
|
||||||
* Round ID for WIN/BET type (null for other types)
|
|
||||||
*/
|
|
||||||
private Long roundId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,5 @@ public class UserCheckDto {
|
|||||||
private Double tickets; // balance_a / 1,000,000
|
private Double tickets; // balance_a / 1,000,000
|
||||||
private Integer depositTotal; // Sum of completed payments stars_amount
|
private Integer depositTotal; // Sum of completed payments stars_amount
|
||||||
private Integer refererId; // referer_id_1 from db_users_d
|
private Integer refererId; // referer_id_1 from db_users_d
|
||||||
private Integer roundsPlayed; // rounds_played from db_users_b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.lottery.lottery.exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thrown when bet amount cannot be decided (e.g. invalid bot range).
|
|
||||||
* Scheduler must not register the bot when this is thrown.
|
|
||||||
*/
|
|
||||||
public class BetDecisionException extends RuntimeException {
|
|
||||||
|
|
||||||
public BetDecisionException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BetDecisionException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.lottery.lottery.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "flexible_bot_configs")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class FlexibleBotConfig {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@Column(name = "user_id", nullable = false)
|
|
||||||
private Integer userId;
|
|
||||||
|
|
||||||
/** Win rate 0.0 to 1.0 (e.g. 0.25 = 25%). */
|
|
||||||
@Column(name = "win_rate", nullable = false, precision = 5, scale = 4)
|
|
||||||
private BigDecimal winRate;
|
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private Instant updatedAt;
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
@PrePersist
|
|
||||||
protected void onUpdate() {
|
|
||||||
updatedAt = Instant.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package com.lottery.lottery.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "game_rooms")
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class GameRoom {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Integer id;
|
|
||||||
|
|
||||||
@Column(name = "room_number", unique = true, nullable = false)
|
|
||||||
private Integer roomNumber;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@Column(name = "current_phase", nullable = false, length = 20, columnDefinition = "VARCHAR(20)")
|
|
||||||
@Builder.Default
|
|
||||||
private GamePhase currentPhase = GamePhase.WAITING;
|
|
||||||
|
|
||||||
@Column(name = "countdown_end_at")
|
|
||||||
private Instant countdownEndAt;
|
|
||||||
|
|
||||||
@Column(name = "total_bet", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Long totalBet = 0L;
|
|
||||||
|
|
||||||
@Column(name = "registered_players", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Integer registeredPlayers = 0;
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private Instant createdAt;
|
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private Instant updatedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
protected void onCreate() {
|
|
||||||
createdAt = Instant.now();
|
|
||||||
updatedAt = Instant.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
protected void onUpdate() {
|
|
||||||
updatedAt = Instant.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package com.lottery.lottery.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "game_rounds")
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class GameRound {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
|
||||||
@JoinColumn(name = "room_id", nullable = false)
|
|
||||||
private GameRoom room;
|
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@Column(name = "phase", nullable = false, length = 20, columnDefinition = "VARCHAR(20)")
|
|
||||||
private GamePhase phase;
|
|
||||||
|
|
||||||
@Column(name = "total_bet", nullable = false)
|
|
||||||
private Long totalBet;
|
|
||||||
|
|
||||||
@Column(name = "winner_user_id")
|
|
||||||
private Integer winnerUserId;
|
|
||||||
|
|
||||||
@Column(name = "winner_bet", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Long winnerBet = 0L;
|
|
||||||
|
|
||||||
@Column(name = "commission", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Long commission = 0L;
|
|
||||||
|
|
||||||
@Column(name = "payout", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Long payout = 0L;
|
|
||||||
|
|
||||||
@Column(name = "started_at", nullable = false)
|
|
||||||
private Instant startedAt;
|
|
||||||
|
|
||||||
@Column(name = "countdown_started_at")
|
|
||||||
private Instant countdownStartedAt;
|
|
||||||
|
|
||||||
@Column(name = "countdown_ended_at")
|
|
||||||
private Instant countdownEndedAt;
|
|
||||||
|
|
||||||
@Column(name = "resolved_at")
|
|
||||||
private Instant resolvedAt;
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private Instant createdAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
protected void onCreate() {
|
|
||||||
createdAt = Instant.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.lottery.lottery.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "game_round_participants")
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class GameRoundParticipant {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
|
||||||
@JoinColumn(name = "round_id", nullable = false)
|
|
||||||
private GameRound round;
|
|
||||||
|
|
||||||
@Column(name = "user_id", nullable = false)
|
|
||||||
private Integer userId;
|
|
||||||
|
|
||||||
@Column(name = "bet", nullable = false)
|
|
||||||
private Long bet;
|
|
||||||
|
|
||||||
@Column(name = "joined_at", nullable = false, updatable = false)
|
|
||||||
private Instant joinedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
protected void onCreate() {
|
|
||||||
joinedAt = Instant.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package com.lottery.lottery.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.time.LocalTime;
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "lottery_bot_configs")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class LotteryBotConfig {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
@Column(name = "id")
|
|
||||||
private Integer id;
|
|
||||||
|
|
||||||
@Column(name = "user_id", nullable = false, unique = true)
|
|
||||||
private Integer userId;
|
|
||||||
|
|
||||||
@Column(name = "room_1", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Boolean room1 = false;
|
|
||||||
|
|
||||||
@Column(name = "room_2", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Boolean room2 = false;
|
|
||||||
|
|
||||||
@Column(name = "room_3", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Boolean room3 = false;
|
|
||||||
|
|
||||||
@Column(name = "time_utc_start", nullable = false)
|
|
||||||
private LocalTime timeUtcStart;
|
|
||||||
|
|
||||||
@Column(name = "time_utc_end", nullable = false)
|
|
||||||
private LocalTime timeUtcEnd;
|
|
||||||
|
|
||||||
@Column(name = "bet_min", nullable = false)
|
|
||||||
private Long betMin;
|
|
||||||
|
|
||||||
@Column(name = "bet_max", nullable = false)
|
|
||||||
private Long betMax;
|
|
||||||
|
|
||||||
@Column(name = "persona", nullable = false, length = 20)
|
|
||||||
@Builder.Default
|
|
||||||
private String persona = "balanced";
|
|
||||||
|
|
||||||
@Column(name = "active", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Boolean active = true;
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
|
||||||
private Instant createdAt;
|
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
|
||||||
private Instant updatedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
protected void onCreate() {
|
|
||||||
Instant now = Instant.now();
|
|
||||||
if (createdAt == null) createdAt = now;
|
|
||||||
if (updatedAt == null) updatedAt = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
protected void onUpdate() {
|
|
||||||
updatedAt = Instant.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.lottery.lottery.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "safe_bot_users")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class SafeBotUser {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@Column(name = "user_id", nullable = false)
|
|
||||||
private Integer userId;
|
|
||||||
}
|
|
||||||
@@ -32,9 +32,6 @@ public class Transaction {
|
|||||||
@Column(name = "task_id")
|
@Column(name = "task_id")
|
||||||
private Integer taskId; // Task ID for TASK_BONUS type
|
private Integer taskId; // Task ID for TASK_BONUS type
|
||||||
|
|
||||||
@Column(name = "round_id")
|
|
||||||
private Long roundId; // Round ID for WIN/BET type
|
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
private Instant createdAt;
|
private Instant createdAt;
|
||||||
|
|
||||||
@@ -49,12 +46,7 @@ public class Transaction {
|
|||||||
public enum TransactionType {
|
public enum TransactionType {
|
||||||
DEPOSIT, // Payment/deposit
|
DEPOSIT, // Payment/deposit
|
||||||
WITHDRAWAL, // Payout/withdrawal
|
WITHDRAWAL, // Payout/withdrawal
|
||||||
WIN, // Game round win (total payout)
|
|
||||||
BET, // Game round bet (for all participants, winners and losers)
|
|
||||||
@Deprecated
|
|
||||||
LOSS, // Legacy: Old bet type, replaced by BET (kept for backward compatibility with old database records)
|
|
||||||
TASK_BONUS, // Task reward
|
TASK_BONUS, // Task reward
|
||||||
DAILY_BONUS, // Daily bonus reward (no taskId)
|
|
||||||
CANCELLATION_OF_WITHDRAWAL // Cancellation of withdrawal (payout cancelled by admin)
|
CANCELLATION_OF_WITHDRAWAL // Cancellation of withdrawal (payout cancelled by admin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,15 +40,6 @@ public class UserB {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Integer withdrawCount = 0;
|
private Integer withdrawCount = 0;
|
||||||
|
|
||||||
@Column(name = "rounds_played", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Integer roundsPlayed = 0;
|
|
||||||
|
|
||||||
/** Total winnings since last deposit (bigint: 1 ticket = 1_000_000). Reset to 0 on deposit; incremented on round win; reduced when payout is created. */
|
|
||||||
@Column(name = "total_win_after_deposit", nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Long totalWinAfterDeposit = 0L;
|
|
||||||
|
|
||||||
/** When true, the user cannot create any payout request (blocked on backend). */
|
/** When true, the user cannot create any payout request (blocked on backend). */
|
||||||
@Column(name = "withdrawals_disabled", nullable = false)
|
@Column(name = "withdrawals_disabled", nullable = false)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package com.lottery.lottery.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "user_daily_bonus_claims")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class UserDailyBonusClaim {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
@Column(name = "id")
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(name = "user_id", nullable = false)
|
|
||||||
private Integer userId;
|
|
||||||
|
|
||||||
@Column(name = "avatar_url", length = 255)
|
|
||||||
private String avatarUrl;
|
|
||||||
|
|
||||||
@Column(name = "screen_name", nullable = false, length = 75)
|
|
||||||
@Builder.Default
|
|
||||||
private String screenName = "-";
|
|
||||||
|
|
||||||
@Column(name = "claimed_at", nullable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
|
|
||||||
private LocalDateTime claimedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
protected void onCreate() {
|
|
||||||
if (claimedAt == null) {
|
|
||||||
claimedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.lottery.lottery.repository;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.FlexibleBotConfig;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface FlexibleBotConfigRepository extends JpaRepository<FlexibleBotConfig, Integer> {
|
|
||||||
|
|
||||||
List<FlexibleBotConfig> findAllByOrderByUserIdAsc();
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.lottery.lottery.repository;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.GamePhase;
|
|
||||||
import com.lottery.lottery.model.GameRoom;
|
|
||||||
import jakarta.persistence.LockModeType;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Lock;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface GameRoomRepository extends JpaRepository<GameRoom, Integer> {
|
|
||||||
Optional<GameRoom> findByRoomNumber(Integer roomNumber);
|
|
||||||
|
|
||||||
// Efficient query for rooms in specific phase (uses index on current_phase)
|
|
||||||
List<GameRoom> findByCurrentPhase(GamePhase phase);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds room by room number with pessimistic write lock to prevent race conditions.
|
|
||||||
* This ensures only one transaction can update the room at a time.
|
|
||||||
*/
|
|
||||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
|
||||||
@Query("SELECT r FROM GameRoom r WHERE r.roomNumber = :roomNumber")
|
|
||||||
Optional<GameRoom> findByRoomNumberWithLock(@Param("roomNumber") Integer roomNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package com.lottery.lottery.repository;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.GameRoundParticipant;
|
|
||||||
import jakarta.persistence.LockModeType;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Lock;
|
|
||||||
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;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface GameRoundParticipantRepository extends JpaRepository<GameRoundParticipant, Long> {
|
|
||||||
List<GameRoundParticipant> findByRoundId(Long roundId);
|
|
||||||
|
|
||||||
@Query("SELECT p FROM GameRoundParticipant p WHERE p.round.id = :roundId AND p.userId = :userId")
|
|
||||||
List<GameRoundParticipant> findByRoundIdAndUserId(@Param("roundId") Long roundId, @Param("userId") Integer userId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds participant by ID with pessimistic write lock to prevent race conditions.
|
|
||||||
* This ensures only one transaction can update the participant at a time.
|
|
||||||
*/
|
|
||||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
|
||||||
@Query("SELECT p FROM GameRoundParticipant p WHERE p.id = :id")
|
|
||||||
Optional<GameRoundParticipant> findByIdWithLock(@Param("id") Long id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all rounds where the user participated, ordered by resolution time (newest first).
|
|
||||||
* Only returns completed rounds (phase = RESOLUTION, resolvedAt IS NOT NULL).
|
|
||||||
*/
|
|
||||||
@Query("SELECT p FROM GameRoundParticipant p " +
|
|
||||||
"WHERE p.userId = :userId " +
|
|
||||||
"AND p.round.phase = 'RESOLUTION' " +
|
|
||||||
"AND p.round.resolvedAt IS NOT NULL " +
|
|
||||||
"ORDER BY p.round.resolvedAt DESC")
|
|
||||||
List<GameRoundParticipant> findUserCompletedRounds(@Param("userId") Integer userId,
|
|
||||||
org.springframework.data.domain.Pageable pageable);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch deletes participants older than the specified date (up to batchSize).
|
|
||||||
* Returns the number of deleted rows.
|
|
||||||
* Note: MySQL requires LIMIT to be used directly in DELETE statements.
|
|
||||||
*/
|
|
||||||
@Modifying(clearAutomatically = true, flushAutomatically = true)
|
|
||||||
@Query(value = "DELETE FROM game_round_participants WHERE joined_at < :cutoffDate LIMIT :batchSize", nativeQuery = true)
|
|
||||||
int deleteOldParticipantsBatch(@Param("cutoffDate") Instant cutoffDate, @Param("batchSize") int batchSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package com.lottery.lottery.repository;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.GameRound;
|
|
||||||
import com.lottery.lottery.model.GameRoom;
|
|
||||||
import com.lottery.lottery.model.GamePhase;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface GameRoundRepository extends JpaRepository<GameRound, Long> {
|
|
||||||
|
|
||||||
/** Fetch rounds by ids with room loaded (for admin game history). */
|
|
||||||
@Query("SELECT r FROM GameRound r LEFT JOIN FETCH r.room WHERE r.id IN :ids")
|
|
||||||
List<GameRound> findAllByIdWithRoom(@Param("ids") Set<Long> ids);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the most recent active round(s) for a room, ordered by startedAt DESC.
|
|
||||||
* Use Pageable with size 1 to get only the single most recent round (resilient to corrupted data with multiple rounds in same phase).
|
|
||||||
*/
|
|
||||||
@Query("SELECT r FROM GameRound r WHERE r.room.id = :roomId AND r.phase IN :phases ORDER BY r.startedAt DESC")
|
|
||||||
List<GameRound> findMostRecentActiveRoundsByRoomId(
|
|
||||||
@Param("roomId") Integer roomId,
|
|
||||||
@Param("phases") List<GamePhase> phases,
|
|
||||||
Pageable pageable
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the last N completed rounds for a room, ordered by resolution time (newest first).
|
|
||||||
* Only returns rounds that have a winner (winner_user_id IS NOT NULL).
|
|
||||||
*/
|
|
||||||
@Query("SELECT r FROM GameRound r WHERE r.room.roomNumber = :roomNumber AND r.phase = 'RESOLUTION' AND r.resolvedAt IS NOT NULL AND r.winnerUserId IS NOT NULL ORDER BY r.resolvedAt DESC")
|
|
||||||
List<GameRound> findLastCompletedRoundsByRoomNumber(
|
|
||||||
@Param("roomNumber") Integer roomNumber,
|
|
||||||
org.springframework.data.domain.Pageable pageable
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts rounds resolved after the specified date.
|
|
||||||
*/
|
|
||||||
long countByResolvedAtAfter(Instant date);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates average total_bet for rounds resolved after the specified date.
|
|
||||||
*/
|
|
||||||
@Query("SELECT AVG(r.totalBet) FROM GameRound r WHERE r.resolvedAt >= :after AND r.resolvedAt IS NOT NULL")
|
|
||||||
Optional<Double> avgTotalBetByResolvedAtAfter(@Param("after") Instant after);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts rounds resolved between two dates.
|
|
||||||
*/
|
|
||||||
long countByResolvedAtBetween(Instant start, Instant end);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.lottery.lottery.repository;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.LotteryBotConfig;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface LotteryBotConfigRepository extends JpaRepository<LotteryBotConfig, Integer> {
|
|
||||||
|
|
||||||
List<LotteryBotConfig> findAllByOrderByIdAsc();
|
|
||||||
|
|
||||||
List<LotteryBotConfig> findAllByActiveTrue();
|
|
||||||
|
|
||||||
List<LotteryBotConfig> findAllByRoom2True();
|
|
||||||
|
|
||||||
List<LotteryBotConfig> findAllByRoom3True();
|
|
||||||
|
|
||||||
Optional<LotteryBotConfig> findByUserId(Integer userId);
|
|
||||||
|
|
||||||
boolean existsByUserId(Integer userId);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.lottery.lottery.repository;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.SafeBotUser;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface SafeBotUserRepository extends JpaRepository<SafeBotUser, Integer> {
|
|
||||||
|
|
||||||
List<SafeBotUser> findAllByOrderByUserIdAsc();
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,6 @@ package com.lottery.lottery.repository;
|
|||||||
import com.lottery.lottery.model.Transaction;
|
import com.lottery.lottery.model.Transaction;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
@@ -12,7 +10,6 @@ import org.springframework.data.repository.query.Param;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@@ -24,13 +21,6 @@ public interface TransactionRepository extends JpaRepository<Transaction, Long>
|
|||||||
*/
|
*/
|
||||||
Page<Transaction> findByUserIdOrderByCreatedAtDesc(Integer userId, Pageable pageable);
|
Page<Transaction> findByUserIdOrderByCreatedAtDesc(Integer userId, Pageable pageable);
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds WIN transactions for a user created after the specified date, ordered by creation time descending.
|
|
||||||
* Used for game history (win history).
|
|
||||||
*/
|
|
||||||
Page<Transaction> findByUserIdAndTypeAndCreatedAtAfterOrderByCreatedAtDesc(
|
|
||||||
Integer userId, Transaction.TransactionType type, Instant createdAfter, Pageable pageable);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch deletes all transactions older than the specified date (up to batchSize).
|
* Batch deletes all transactions older than the specified date (up to batchSize).
|
||||||
* Returns the number of deleted rows.
|
* Returns the number of deleted rows.
|
||||||
@@ -42,7 +32,6 @@ public interface TransactionRepository extends JpaRepository<Transaction, Long>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts transactions of a specific type for a user.
|
* Counts transactions of a specific type for a user.
|
||||||
* Used to check if this is the user's 3rd bet for referral bonus.
|
|
||||||
*/
|
*/
|
||||||
long countByUserIdAndType(Integer userId, Transaction.TransactionType type);
|
long countByUserIdAndType(Integer userId, Transaction.TransactionType type);
|
||||||
|
|
||||||
@@ -52,11 +41,5 @@ public interface TransactionRepository extends JpaRepository<Transaction, Long>
|
|||||||
@Query("SELECT t.userId, COALESCE(SUM(t.amount), 0) FROM Transaction t WHERE t.userId IN :userIds GROUP BY t.userId")
|
@Query("SELECT t.userId, COALESCE(SUM(t.amount), 0) FROM Transaction t WHERE t.userId IN :userIds GROUP BY t.userId")
|
||||||
List<Object[]> sumAmountByUserIdIn(@Param("userIds") List<Integer> userIds);
|
List<Object[]> sumAmountByUserIdIn(@Param("userIds") List<Integer> userIds);
|
||||||
|
|
||||||
/** BET transactions for a user, ordered by createdAt desc (for game history). */
|
|
||||||
Page<Transaction> findByUserIdAndTypeOrderByCreatedAtDesc(Integer userId, Transaction.TransactionType type, Pageable pageable);
|
|
||||||
|
|
||||||
/** WIN transactions for a user and given round IDs (batch). */
|
|
||||||
@Query("SELECT t FROM Transaction t WHERE t.userId = :userId AND t.type = 'WIN' AND t.roundId IN :roundIds")
|
|
||||||
List<Transaction> findByUserIdAndTypeWinAndRoundIdIn(@Param("userId") Integer userId, @Param("roundIds") Set<Long> roundIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package com.lottery.lottery.repository;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.UserDailyBonusClaim;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface UserDailyBonusClaimRepository extends JpaRepository<UserDailyBonusClaim, Long> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the most recent daily bonus claim for a user.
|
|
||||||
* Used to check if user can claim (24h cooldown).
|
|
||||||
*/
|
|
||||||
Optional<UserDailyBonusClaim> findFirstByUserIdOrderByClaimedAtDesc(Integer userId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the 50 most recent daily bonus claims ordered by claimed_at DESC.
|
|
||||||
* Simple query without JOINs - all data is in the same table.
|
|
||||||
*/
|
|
||||||
List<UserDailyBonusClaim> findTop50ByOrderByClaimedAtDesc();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all daily bonus claims for a user, ordered by claimed_at DESC.
|
|
||||||
*/
|
|
||||||
List<UserDailyBonusClaim> findByUserIdOrderByClaimedAtDesc(Integer userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
import com.lottery.lottery.dto.AdminBotConfigDto;
|
|
||||||
import com.lottery.lottery.dto.AdminBotConfigRequest;
|
|
||||||
import com.lottery.lottery.model.LotteryBotConfig;
|
|
||||||
import com.lottery.lottery.model.UserA;
|
|
||||||
import com.lottery.lottery.repository.LotteryBotConfigRepository;
|
|
||||||
import com.lottery.lottery.repository.UserARepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AdminBotConfigService {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
|
|
||||||
|
|
||||||
private final LotteryBotConfigRepository lotteryBotConfigRepository;
|
|
||||||
private final UserARepository userARepository;
|
|
||||||
|
|
||||||
public List<AdminBotConfigDto> listAll() {
|
|
||||||
List<LotteryBotConfig> configs = lotteryBotConfigRepository.findAllByOrderByIdAsc();
|
|
||||||
if (configs.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<Integer> userIds = configs.stream().map(LotteryBotConfig::getUserId).distinct().toList();
|
|
||||||
Map<Integer, String> screenNameByUserId = userARepository.findAllById(userIds).stream()
|
|
||||||
.collect(Collectors.toMap(UserA::getId, u -> u.getScreenName() != null ? u.getScreenName() : "-"));
|
|
||||||
return configs.stream()
|
|
||||||
.map(c -> toDto(c, screenNameByUserId.getOrDefault(c.getUserId(), "-")))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AdminBotConfigDto> getById(Integer id) {
|
|
||||||
return lotteryBotConfigRepository.findById(id)
|
|
||||||
.map(c -> {
|
|
||||||
String screenName = userARepository.findById(c.getUserId())
|
|
||||||
.map(UserA::getScreenName)
|
|
||||||
.orElse("-");
|
|
||||||
return toDto(c, screenName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AdminBotConfigDto> getByUserId(Integer userId) {
|
|
||||||
return lotteryBotConfigRepository.findByUserId(userId)
|
|
||||||
.map(c -> {
|
|
||||||
String screenName = userARepository.findById(c.getUserId())
|
|
||||||
.map(UserA::getScreenName)
|
|
||||||
.orElse("-");
|
|
||||||
return toDto(c, screenName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public AdminBotConfigDto create(AdminBotConfigRequest request) {
|
|
||||||
if (!userARepository.existsById(request.getUserId())) {
|
|
||||||
throw new IllegalArgumentException("User with id " + request.getUserId() + " does not exist");
|
|
||||||
}
|
|
||||||
if (lotteryBotConfigRepository.existsByUserId(request.getUserId())) {
|
|
||||||
throw new IllegalArgumentException("Bot config already exists for user id " + request.getUserId());
|
|
||||||
}
|
|
||||||
LotteryBotConfig config = toEntity(request);
|
|
||||||
config.setId(null);
|
|
||||||
config.setCreatedAt(Instant.now());
|
|
||||||
config.setUpdatedAt(Instant.now());
|
|
||||||
config = lotteryBotConfigRepository.save(config);
|
|
||||||
String screenName = userARepository.findById(config.getUserId()).map(UserA::getScreenName).orElse("-");
|
|
||||||
return toDto(config, screenName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public Optional<AdminBotConfigDto> update(Integer id, AdminBotConfigRequest request) {
|
|
||||||
Optional<LotteryBotConfig> opt = lotteryBotConfigRepository.findById(id);
|
|
||||||
if (opt.isEmpty()) return Optional.empty();
|
|
||||||
if (!userARepository.existsById(request.getUserId())) {
|
|
||||||
throw new IllegalArgumentException("User with id " + request.getUserId() + " does not exist");
|
|
||||||
}
|
|
||||||
LotteryBotConfig existing = opt.get();
|
|
||||||
if (!existing.getUserId().equals(request.getUserId()) && lotteryBotConfigRepository.existsByUserId(request.getUserId())) {
|
|
||||||
throw new IllegalArgumentException("Bot config already exists for user id " + request.getUserId());
|
|
||||||
}
|
|
||||||
updateEntity(existing, request);
|
|
||||||
existing.setUpdatedAt(Instant.now());
|
|
||||||
LotteryBotConfig saved = lotteryBotConfigRepository.save(existing);
|
|
||||||
String screenName = userARepository.findById(saved.getUserId()).map(UserA::getScreenName).orElse("-");
|
|
||||||
return Optional.of(toDto(saved, screenName));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public boolean delete(Integer id) {
|
|
||||||
if (!lotteryBotConfigRepository.existsById(id)) return false;
|
|
||||||
lotteryBotConfigRepository.deleteById(id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shuffles time windows for bots that have the given room enabled.
|
|
||||||
* Groups configs by their current time window, then randomly redistributes those same windows across all configs.
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public void shuffleTimeWindowsForRoom(int roomNumber) {
|
|
||||||
List<LotteryBotConfig> configs = roomNumber == 2
|
|
||||||
? lotteryBotConfigRepository.findAllByRoom2True()
|
|
||||||
: lotteryBotConfigRepository.findAllByRoom3True();
|
|
||||||
if (configs.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("No bot configs with room " + roomNumber + " enabled");
|
|
||||||
}
|
|
||||||
shuffleWindows(configs);
|
|
||||||
Instant now = Instant.now();
|
|
||||||
for (LotteryBotConfig c : configs) {
|
|
||||||
c.setUpdatedAt(now);
|
|
||||||
}
|
|
||||||
lotteryBotConfigRepository.saveAll(configs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Groups configs by (start, end) window, collects one slot per config, shuffles slots, assigns back. */
|
|
||||||
private static void shuffleWindows(List<LotteryBotConfig> configs) {
|
|
||||||
Map<TimeWindow, List<LotteryBotConfig>> byWindow = new LinkedHashMap<>();
|
|
||||||
for (LotteryBotConfig c : configs) {
|
|
||||||
if (c.getTimeUtcStart() == null || c.getTimeUtcEnd() == null) continue;
|
|
||||||
TimeWindow w = new TimeWindow(c.getTimeUtcStart(), c.getTimeUtcEnd());
|
|
||||||
byWindow.computeIfAbsent(w, k -> new ArrayList<>()).add(c);
|
|
||||||
}
|
|
||||||
List<TimeWindow> windowSlots = new ArrayList<>();
|
|
||||||
for (Map.Entry<TimeWindow, List<LotteryBotConfig>> e : byWindow.entrySet()) {
|
|
||||||
for (int i = 0; i < e.getValue().size(); i++) {
|
|
||||||
windowSlots.add(e.getKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Collections.shuffle(windowSlots);
|
|
||||||
int idx = 0;
|
|
||||||
for (LotteryBotConfig c : configs) {
|
|
||||||
if (c.getTimeUtcStart() != null && c.getTimeUtcEnd() != null && idx < windowSlots.size()) {
|
|
||||||
TimeWindow w = windowSlots.get(idx++);
|
|
||||||
c.setTimeUtcStart(w.start);
|
|
||||||
c.setTimeUtcEnd(w.end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private record TimeWindow(LocalTime start, LocalTime end) {}
|
|
||||||
|
|
||||||
private static AdminBotConfigDto toDto(LotteryBotConfig c, String screenName) {
|
|
||||||
return AdminBotConfigDto.builder()
|
|
||||||
.id(c.getId())
|
|
||||||
.userId(c.getUserId())
|
|
||||||
.screenName(screenName)
|
|
||||||
.room1(c.getRoom1())
|
|
||||||
.room2(c.getRoom2())
|
|
||||||
.room3(c.getRoom3())
|
|
||||||
.timeUtcStart(c.getTimeUtcStart() != null ? c.getTimeUtcStart().format(TIME_FORMAT) : null)
|
|
||||||
.timeUtcEnd(c.getTimeUtcEnd() != null ? c.getTimeUtcEnd().format(TIME_FORMAT) : null)
|
|
||||||
.betMin(c.getBetMin())
|
|
||||||
.betMax(c.getBetMax())
|
|
||||||
.persona(c.getPersona() != null ? c.getPersona() : "balanced")
|
|
||||||
.active(c.getActive())
|
|
||||||
.createdAt(c.getCreatedAt())
|
|
||||||
.updatedAt(c.getUpdatedAt())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LotteryBotConfig toEntity(AdminBotConfigRequest r) {
|
|
||||||
return LotteryBotConfig.builder()
|
|
||||||
.userId(r.getUserId())
|
|
||||||
.room1(r.getRoom1())
|
|
||||||
.room2(r.getRoom2())
|
|
||||||
.room3(r.getRoom3())
|
|
||||||
.timeUtcStart(parseTime(r.getTimeUtcStart()))
|
|
||||||
.timeUtcEnd(parseTime(r.getTimeUtcEnd()))
|
|
||||||
.betMin(r.getBetMin())
|
|
||||||
.betMax(r.getBetMax())
|
|
||||||
.persona(r.getPersona() != null && !r.getPersona().isBlank() ? r.getPersona() : "balanced")
|
|
||||||
.active(r.getActive())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void updateEntity(LotteryBotConfig existing, AdminBotConfigRequest r) {
|
|
||||||
existing.setUserId(r.getUserId());
|
|
||||||
existing.setRoom1(r.getRoom1());
|
|
||||||
existing.setRoom2(r.getRoom2());
|
|
||||||
existing.setRoom3(r.getRoom3());
|
|
||||||
existing.setTimeUtcStart(parseTime(r.getTimeUtcStart()));
|
|
||||||
existing.setTimeUtcEnd(parseTime(r.getTimeUtcEnd()));
|
|
||||||
existing.setBetMin(r.getBetMin());
|
|
||||||
existing.setBetMax(r.getBetMax());
|
|
||||||
existing.setPersona(r.getPersona() != null && !r.getPersona().isBlank() ? r.getPersona() : "balanced");
|
|
||||||
existing.setActive(r.getActive());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocalTime parseTime(String s) {
|
|
||||||
if (s == null || s.isBlank()) throw new IllegalArgumentException("Time is required (HH:mm)");
|
|
||||||
try {
|
|
||||||
return LocalTime.parse(s.trim(), TIME_FORMAT);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new IllegalArgumentException("Invalid time format, use HH:mm (e.g. 14:00)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,14 +42,11 @@ public class AdminUserService {
|
|||||||
private final UserBRepository userBRepository;
|
private final UserBRepository userBRepository;
|
||||||
private final UserDRepository userDRepository;
|
private final UserDRepository userDRepository;
|
||||||
private final TransactionRepository transactionRepository;
|
private final TransactionRepository transactionRepository;
|
||||||
private final GameRoundParticipantRepository gameRoundParticipantRepository;
|
|
||||||
private final PaymentRepository paymentRepository;
|
private final PaymentRepository paymentRepository;
|
||||||
private final PayoutRepository payoutRepository;
|
private final PayoutRepository payoutRepository;
|
||||||
private final UserTaskClaimRepository userTaskClaimRepository;
|
private final UserTaskClaimRepository userTaskClaimRepository;
|
||||||
private final TaskRepository taskRepository;
|
private final TaskRepository taskRepository;
|
||||||
private final UserDailyBonusClaimRepository userDailyBonusClaimRepository;
|
|
||||||
private final EntityManager entityManager;
|
private final EntityManager entityManager;
|
||||||
private final GameRoundRepository gameRoundRepository;
|
|
||||||
|
|
||||||
public Page<AdminUserDto> getUsers(
|
public Page<AdminUserDto> getUsers(
|
||||||
Pageable pageable,
|
Pageable pageable,
|
||||||
@@ -61,8 +58,6 @@ public class AdminUserService {
|
|||||||
Integer dateRegTo,
|
Integer dateRegTo,
|
||||||
Long balanceMin,
|
Long balanceMin,
|
||||||
Long balanceMax,
|
Long balanceMax,
|
||||||
Integer roundsPlayedMin,
|
|
||||||
Integer roundsPlayedMax,
|
|
||||||
Integer referralCountMin,
|
Integer referralCountMin,
|
||||||
Integer referralCountMax,
|
Integer referralCountMax,
|
||||||
Integer referrerId,
|
Integer referrerId,
|
||||||
@@ -142,8 +137,8 @@ public class AdminUserService {
|
|||||||
predicates.add(cb.lessThanOrEqualTo(root.get("dateReg"), endOfDayTimestamp));
|
predicates.add(cb.lessThanOrEqualTo(root.get("dateReg"), endOfDayTimestamp));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Balance / rounds / referral filters via subqueries so DB handles pagination
|
// Balance / referral filters via subqueries so DB handles pagination
|
||||||
if (balanceMin != null || balanceMax != null || roundsPlayedMin != null || roundsPlayedMax != null) {
|
if (balanceMin != null || balanceMax != null) {
|
||||||
Subquery<Integer> subB = query.subquery(Integer.class);
|
Subquery<Integer> subB = query.subquery(Integer.class);
|
||||||
Root<UserB> br = subB.from(UserB.class);
|
Root<UserB> br = subB.from(UserB.class);
|
||||||
subB.select(br.get("id"));
|
subB.select(br.get("id"));
|
||||||
@@ -156,8 +151,6 @@ public class AdminUserService {
|
|||||||
subPreds.add(cb.lessThanOrEqualTo(
|
subPreds.add(cb.lessThanOrEqualTo(
|
||||||
cb.sum(br.get("balanceA"), br.get("balanceB")), balanceMax));
|
cb.sum(br.get("balanceA"), br.get("balanceB")), balanceMax));
|
||||||
}
|
}
|
||||||
if (roundsPlayedMin != null) subPreds.add(cb.greaterThanOrEqualTo(br.get("roundsPlayed"), roundsPlayedMin));
|
|
||||||
if (roundsPlayedMax != null) subPreds.add(cb.lessThanOrEqualTo(br.get("roundsPlayed"), roundsPlayedMax));
|
|
||||||
subB.where(cb.and(subPreds.toArray(new Predicate[0])));
|
subB.where(cb.and(subPreds.toArray(new Predicate[0])));
|
||||||
predicates.add(cb.in(root.get("id")).value(subB));
|
predicates.add(cb.in(root.get("id")).value(subB));
|
||||||
}
|
}
|
||||||
@@ -193,7 +186,7 @@ public class AdminUserService {
|
|||||||
return cb.and(predicates.toArray(new Predicate[0]));
|
return cb.and(predicates.toArray(new Predicate[0]));
|
||||||
};
|
};
|
||||||
|
|
||||||
Set<String> sortRequiresJoin = Set.of("balanceA", "depositTotal", "withdrawTotal", "roundsPlayed", "referralCount", "profit");
|
Set<String> sortRequiresJoin = Set.of("balanceA", "depositTotal", "withdrawTotal", "referralCount", "profit");
|
||||||
boolean useJoinSort = sortBy != null && sortRequiresJoin.contains(sortBy);
|
boolean useJoinSort = sortBy != null && sortRequiresJoin.contains(sortBy);
|
||||||
List<UserA> userList;
|
List<UserA> userList;
|
||||||
long totalElements;
|
long totalElements;
|
||||||
@@ -202,7 +195,7 @@ public class AdminUserService {
|
|||||||
List<Integer> orderedIds = getOrderedUserIdsForAdminList(
|
List<Integer> orderedIds = getOrderedUserIdsForAdminList(
|
||||||
search, banned, countryCode, languageCode,
|
search, banned, countryCode, languageCode,
|
||||||
dateRegFrom, dateRegTo, balanceMin, balanceMax,
|
dateRegFrom, dateRegTo, balanceMin, balanceMax,
|
||||||
roundsPlayedMin, roundsPlayedMax, referralCountMin, referralCountMax,
|
referralCountMin, referralCountMax,
|
||||||
referrerId, referralLevel, ipFilter,
|
referrerId, referralLevel, ipFilter,
|
||||||
sortBy, sortDir != null ? sortDir : "desc",
|
sortBy, sortDir != null ? sortDir : "desc",
|
||||||
pageable.getPageSize(), (int) pageable.getOffset(),
|
pageable.getPageSize(), (int) pageable.getOffset(),
|
||||||
@@ -242,7 +235,6 @@ public class AdminUserService {
|
|||||||
.depositCount(0)
|
.depositCount(0)
|
||||||
.withdrawTotal(0L)
|
.withdrawTotal(0L)
|
||||||
.withdrawCount(0)
|
.withdrawCount(0)
|
||||||
.roundsPlayed(0)
|
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
UserD userD = userDMap.getOrDefault(userA.getId(),
|
UserD userD = userDMap.getOrDefault(userA.getId(),
|
||||||
@@ -280,7 +272,6 @@ public class AdminUserService {
|
|||||||
.depositCount(userB.getDepositCount())
|
.depositCount(userB.getDepositCount())
|
||||||
.withdrawTotal(userB.getWithdrawTotal())
|
.withdrawTotal(userB.getWithdrawTotal())
|
||||||
.withdrawCount(userB.getWithdrawCount())
|
.withdrawCount(userB.getWithdrawCount())
|
||||||
.roundsPlayed(userB.getRoundsPlayed())
|
|
||||||
.dateReg(userA.getDateReg())
|
.dateReg(userA.getDateReg())
|
||||||
.dateLogin(userA.getDateLogin())
|
.dateLogin(userA.getDateLogin())
|
||||||
.banned(userA.getBanned())
|
.banned(userA.getBanned())
|
||||||
@@ -312,8 +303,6 @@ public class AdminUserService {
|
|||||||
Integer dateRegTo,
|
Integer dateRegTo,
|
||||||
Long balanceMin,
|
Long balanceMin,
|
||||||
Long balanceMax,
|
Long balanceMax,
|
||||||
Integer roundsPlayedMin,
|
|
||||||
Integer roundsPlayedMax,
|
|
||||||
Integer referralCountMin,
|
Integer referralCountMin,
|
||||||
Integer referralCountMax,
|
Integer referralCountMax,
|
||||||
Integer referrerId,
|
Integer referrerId,
|
||||||
@@ -395,16 +384,6 @@ public class AdminUserService {
|
|||||||
params.add(balanceMax);
|
params.add(balanceMax);
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
if (roundsPlayedMin != null) {
|
|
||||||
sql.append(" AND b.rounds_played >= ?");
|
|
||||||
params.add(roundsPlayedMin);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
if (roundsPlayedMax != null) {
|
|
||||||
sql.append(" AND b.rounds_played <= ?");
|
|
||||||
params.add(roundsPlayedMax);
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
if (referralCountMin != null || referralCountMax != null) {
|
if (referralCountMin != null || referralCountMax != null) {
|
||||||
sql.append(" AND (d.referals_1 + d.referals_2 + d.referals_3 + d.referals_4 + d.referals_5)");
|
sql.append(" AND (d.referals_1 + d.referals_2 + d.referals_3 + d.referals_4 + d.referals_5)");
|
||||||
if (referralCountMin != null && referralCountMax != null) {
|
if (referralCountMin != null && referralCountMax != null) {
|
||||||
@@ -436,11 +415,10 @@ public class AdminUserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String orderColumn = switch (sortBy) {
|
String orderColumn = switch (sortBy != null ? sortBy : "") {
|
||||||
case "balanceA" -> "b.balance_a";
|
case "balanceA" -> "b.balance_a";
|
||||||
case "depositTotal" -> "b.deposit_total";
|
case "depositTotal" -> "b.deposit_total";
|
||||||
case "withdrawTotal" -> "b.withdraw_total";
|
case "withdrawTotal" -> "b.withdraw_total";
|
||||||
case "roundsPlayed" -> "b.rounds_played";
|
|
||||||
case "referralCount" -> "(d.referals_1 + d.referals_2 + d.referals_3 + d.referals_4 + d.referals_5)";
|
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 "profit" -> "(b.deposit_total - b.withdraw_total)";
|
||||||
default -> "a.id";
|
default -> "a.id";
|
||||||
@@ -506,8 +484,6 @@ public class AdminUserService {
|
|||||||
.depositCount(0)
|
.depositCount(0)
|
||||||
.withdrawTotal(0L)
|
.withdrawTotal(0L)
|
||||||
.withdrawCount(0)
|
.withdrawCount(0)
|
||||||
.roundsPlayed(0)
|
|
||||||
.totalWinAfterDeposit(0L)
|
|
||||||
.withdrawalsDisabled(false)
|
.withdrawalsDisabled(false)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
@@ -610,7 +586,6 @@ public class AdminUserService {
|
|||||||
.depositTotalUsd(depositTotalUsd)
|
.depositTotalUsd(depositTotalUsd)
|
||||||
.withdrawTotalUsd(withdrawTotalUsd)
|
.withdrawTotalUsd(withdrawTotalUsd)
|
||||||
.withdrawalsDisabled(Boolean.TRUE.equals(userB.getWithdrawalsDisabled()))
|
.withdrawalsDisabled(Boolean.TRUE.equals(userB.getWithdrawalsDisabled()))
|
||||||
.roundsPlayed(userB.getRoundsPlayed())
|
|
||||||
.referralCount(totalReferrals)
|
.referralCount(totalReferrals)
|
||||||
.totalCommissionsEarned(totalCommissions)
|
.totalCommissionsEarned(totalCommissions)
|
||||||
.totalCommissionsEarnedUsd(totalCommissionsEarnedUsd)
|
.totalCommissionsEarnedUsd(totalCommissionsEarnedUsd)
|
||||||
@@ -665,53 +640,6 @@ public class AdminUserService {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Game history from transactions (BET/WIN). Participants table is cleaned after each round.
|
|
||||||
*/
|
|
||||||
public Page<AdminGameRoundDto> getUserGameRounds(Integer userId, Pageable pageable) {
|
|
||||||
Page<Transaction> betPage = transactionRepository.findByUserIdAndTypeOrderByCreatedAtDesc(userId, Transaction.TransactionType.BET, pageable);
|
|
||||||
List<Transaction> bets = betPage.getContent();
|
|
||||||
if (bets.isEmpty()) {
|
|
||||||
return new PageImpl<>(List.of(), pageable, 0);
|
|
||||||
}
|
|
||||||
Set<Long> roundIds = bets.stream().map(Transaction::getRoundId).filter(java.util.Objects::nonNull).collect(Collectors.toSet());
|
|
||||||
List<Transaction> wins = roundIds.isEmpty() ? List.of() : transactionRepository.findByUserIdAndTypeWinAndRoundIdIn(userId, roundIds);
|
|
||||||
Map<Long, Long> payoutByRound = wins.stream().collect(Collectors.toMap(Transaction::getRoundId, t -> t.getAmount() != null ? t.getAmount() : 0L, (a, b) -> a));
|
|
||||||
Map<Long, Instant> resolvedAtByRound = wins.stream().collect(Collectors.toMap(Transaction::getRoundId, t -> t.getCreatedAt() != null ? t.getCreatedAt() : Instant.EPOCH, (a, b) -> a));
|
|
||||||
|
|
||||||
Map<Long, GameRound> roundById = roundIds.isEmpty() ? Map.of() : gameRoundRepository.findAllByIdWithRoom(roundIds).stream()
|
|
||||||
.collect(Collectors.toMap(GameRound::getId, r -> r, (a, b) -> a));
|
|
||||||
|
|
||||||
List<AdminGameRoundDto> rounds = bets.stream()
|
|
||||||
.map(bet -> {
|
|
||||||
Long roundId = bet.getRoundId();
|
|
||||||
GameRound gr = roundById.get(roundId);
|
|
||||||
Integer roomNumber = gr != null && gr.getRoom() != null ? gr.getRoom().getRoomNumber() : null;
|
|
||||||
Long totalBet = gr != null ? gr.getTotalBet() : null;
|
|
||||||
long userBet = bet.getAmount() != null ? Math.abs(bet.getAmount()) : 0L;
|
|
||||||
Long payout = payoutByRound.getOrDefault(roundId, 0L);
|
|
||||||
boolean isWinner = payout > 0;
|
|
||||||
Instant resolvedAt = resolvedAtByRound.getOrDefault(roundId, bet.getCreatedAt());
|
|
||||||
return AdminGameRoundDto.builder()
|
|
||||||
.roundId(roundId)
|
|
||||||
.roomNumber(roomNumber)
|
|
||||||
.phase(gr != null && gr.getPhase() != null ? gr.getPhase().name() : null)
|
|
||||||
.totalBet(totalBet)
|
|
||||||
.userBet(userBet)
|
|
||||||
.winnerUserId(isWinner ? userId : null)
|
|
||||||
.winnerBet(isWinner ? userBet : null)
|
|
||||||
.payout(isWinner ? payout : 0L)
|
|
||||||
.commission(null)
|
|
||||||
.startedAt(null)
|
|
||||||
.resolvedAt(resolvedAt)
|
|
||||||
.isWinner(isWinner)
|
|
||||||
.build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return new PageImpl<>(rounds, pageable, betPage.getTotalElements());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> getUserTasks(Integer userId) {
|
public Map<String, Object> getUserTasks(Integer userId) {
|
||||||
List<UserTaskClaim> claims = userTaskClaimRepository.findByUserId(userId);
|
List<UserTaskClaim> claims = userTaskClaimRepository.findByUserId(userId);
|
||||||
List<Task> allTasks = taskRepository.findAll();
|
List<Task> allTasks = taskRepository.findAll();
|
||||||
@@ -749,25 +677,9 @@ public class AdminUserService {
|
|||||||
))
|
))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// Get daily bonus claims
|
|
||||||
List<UserDailyBonusClaim> dailyBonusClaims = userDailyBonusClaimRepository.findByUserIdOrderByClaimedAtDesc(userId);
|
|
||||||
List<Map<String, Object>> dailyBonuses = dailyBonusClaims.stream()
|
|
||||||
.map(claim -> {
|
|
||||||
Instant claimedAtInstant = claim.getClaimedAt() != null
|
|
||||||
? claim.getClaimedAt().atZone(ZoneId.of("UTC")).toInstant()
|
|
||||||
: null;
|
|
||||||
return Map.<String, Object>of(
|
|
||||||
"id", claim.getId(),
|
|
||||||
"claimedAt", claimedAtInstant != null ? claimedAtInstant.toEpochMilli() : null,
|
|
||||||
"screenName", claim.getScreenName() != null ? claim.getScreenName() : "-"
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"completed", completedTasks,
|
"completed", completedTasks,
|
||||||
"available", availableTasks,
|
"available", availableTasks
|
||||||
"dailyBonuses", dailyBonuses
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,7 +717,6 @@ public class AdminUserService {
|
|||||||
.depositCount(0)
|
.depositCount(0)
|
||||||
.withdrawTotal(0L)
|
.withdrawTotal(0L)
|
||||||
.withdrawCount(0)
|
.withdrawCount(0)
|
||||||
.roundsPlayed(0)
|
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
// Store previous balances
|
// Store previous balances
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decides bet amount (in tickets) for a lottery bot.
|
|
||||||
* Implemented in-process by persona + loss-streak and zone logic (no external API).
|
|
||||||
*/
|
|
||||||
public interface BetDecisionService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns bet amount in tickets (1 ticket = 1_000_000 in DB bigint).
|
|
||||||
* Must be within room min/max; caller will clamp if needed.
|
|
||||||
*
|
|
||||||
* @param context room, round, config, etc. (for future AI)
|
|
||||||
* @return bet amount in tickets (e.g. 1000)
|
|
||||||
*/
|
|
||||||
long decideBetAmountTickets(BotBetContext context);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.LotteryBotConfig;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context passed to bet decision (ChatGPT). Bot range and history are required for the prompt.
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
public class BotBetContext {
|
|
||||||
|
|
||||||
private int roomNumber;
|
|
||||||
private Long roundId;
|
|
||||||
private int participantCount;
|
|
||||||
private LotteryBotConfig config;
|
|
||||||
|
|
||||||
/** Room min/max bet in tickets. */
|
|
||||||
private long roomMinTickets;
|
|
||||||
private long roomMaxTickets;
|
|
||||||
/** Bot config min/max bet in tickets (hard bounds for output). */
|
|
||||||
private long botMinTickets;
|
|
||||||
private long botMaxTickets;
|
|
||||||
/** Current round total bet in tickets (pot so far). */
|
|
||||||
private long currentRoundTotalBetTickets;
|
|
||||||
/** Last 10 bet amounts in tickets (oldest → newest). Padded with 0 if fewer than 10. */
|
|
||||||
private List<Integer> lastBets10;
|
|
||||||
/** Last 10 results: W=win, L=loss, N=no data (oldest → newest). Padded with N if fewer than 10. */
|
|
||||||
private List<String> lastResults10;
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.Transaction;
|
|
||||||
import com.lottery.lottery.repository.TransactionRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads last N bet amounts and win/loss results for a user from transactions (BET + WIN).
|
|
||||||
* Used to build ChatGPT prompt context.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class BotBetHistoryService {
|
|
||||||
|
|
||||||
private static final long TICKETS_TO_BIGINT = 1_000_000L;
|
|
||||||
private static final String RESULT_WIN = "W";
|
|
||||||
private static final String RESULT_LOSS = "L";
|
|
||||||
private static final String RESULT_NONE = "N";
|
|
||||||
|
|
||||||
private final TransactionRepository transactionRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns last {@code count} bet amounts (in tickets) and results (W/L/N), oldest first.
|
|
||||||
* If fewer than count bets exist, pads with 0 and "N".
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public BetHistoryResult getLastBetsAndResults(int userId, int count) {
|
|
||||||
if (count <= 0) {
|
|
||||||
return new BetHistoryResult(Collections.nCopies(count, 0), Collections.nCopies(count, RESULT_NONE));
|
|
||||||
}
|
|
||||||
List<Transaction> betTxs = transactionRepository
|
|
||||||
.findByUserIdAndTypeOrderByCreatedAtDesc(userId, Transaction.TransactionType.BET, PageRequest.of(0, count))
|
|
||||||
.getContent();
|
|
||||||
if (betTxs.isEmpty()) {
|
|
||||||
return new BetHistoryResult(
|
|
||||||
Collections.nCopies(count, 0),
|
|
||||||
Collections.nCopies(count, RESULT_NONE));
|
|
||||||
}
|
|
||||||
// Newest first → reverse to oldest first
|
|
||||||
List<Transaction> oldestFirst = new ArrayList<>(betTxs);
|
|
||||||
Collections.reverse(oldestFirst);
|
|
||||||
|
|
||||||
List<Integer> bets = new ArrayList<>();
|
|
||||||
List<Long> roundIds = new ArrayList<>();
|
|
||||||
for (Transaction t : oldestFirst) {
|
|
||||||
// BET amounts are stored as negative (debit); use abs for ticket count in prompt
|
|
||||||
long amount = t.getAmount() != null ? Math.abs(t.getAmount()) : 0L;
|
|
||||||
long tickets = amount / TICKETS_TO_BIGINT;
|
|
||||||
bets.add((int) Math.max(0, Math.min(Integer.MAX_VALUE, tickets)));
|
|
||||||
roundIds.add(t.getRoundId());
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<Long> roundIdsToCheck = roundIds.stream().filter(id -> id != null).collect(Collectors.toSet());
|
|
||||||
Set<Long> roundIdsWithWin = Set.of();
|
|
||||||
if (!roundIdsToCheck.isEmpty()) {
|
|
||||||
List<Transaction> winTxs = transactionRepository.findByUserIdAndTypeWinAndRoundIdIn(userId, roundIdsToCheck);
|
|
||||||
roundIdsWithWin = winTxs.stream().map(Transaction::getRoundId).filter(id -> id != null).collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> results = new ArrayList<>();
|
|
||||||
for (Long roundId : roundIds) {
|
|
||||||
results.add(roundId != null && roundIdsWithWin.contains(roundId) ? RESULT_WIN : RESULT_LOSS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left-pad to count so format is [oldest …, newest]
|
|
||||||
while (bets.size() < count) {
|
|
||||||
bets.add(0, 0);
|
|
||||||
results.add(0, RESULT_NONE);
|
|
||||||
}
|
|
||||||
return new BetHistoryResult(bets, results);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record BetHistoryResult(List<Integer> lastBets, List<String> lastResults) {}
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.FlexibleBotConfig;
|
|
||||||
import com.lottery.lottery.model.GameRoundParticipant;
|
|
||||||
import com.lottery.lottery.model.SafeBotUser;
|
|
||||||
import com.lottery.lottery.model.UserB;
|
|
||||||
import com.lottery.lottery.repository.FlexibleBotConfigRepository;
|
|
||||||
import com.lottery.lottery.repository.SafeBotUserRepository;
|
|
||||||
import com.lottery.lottery.repository.UserBRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bot configuration and winner-override logic for safe/flexible bots.
|
|
||||||
* Does not affect displayed chances or tape; only who is selected as winner at resolution.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class BotConfigService {
|
|
||||||
|
|
||||||
/** Balance below this (bigint: 1 ticket = 1_000_000) → safe bot gets 100% win. 20_000 tickets = 20$ */
|
|
||||||
private static final long SAFE_BOT_BALANCE_THRESHOLD = 20_000L * 1_000_000L; // 20_000_000_000
|
|
||||||
|
|
||||||
private final SafeBotUserRepository safeBotUserRepository;
|
|
||||||
private final FlexibleBotConfigRepository flexibleBotConfigRepository;
|
|
||||||
private final UserBRepository userBRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If a bot override applies, returns the participant to use as winner; otherwise empty (use normal weighted random).
|
|
||||||
* Order: 1) Safe bot with balance < threshold wins. 2) Flexible bot with configured win rate. 3) Normal.
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public Optional<GameRoundParticipant> resolveWinnerOverride(
|
|
||||||
List<GameRoundParticipant> participants,
|
|
||||||
long totalBet
|
|
||||||
) {
|
|
||||||
if (participants == null || participants.isEmpty()) return Optional.empty();
|
|
||||||
|
|
||||||
Set<Integer> safeBotUserIds = getSafeBotUserIds();
|
|
||||||
Map<Integer, Double> flexibleWinRates = getFlexibleBotWinRates();
|
|
||||||
|
|
||||||
// 1) Safe bot: any safe bot in round with balance < threshold wins (pick one randomly if multiple)
|
|
||||||
List<GameRoundParticipant> safeBotsInRound = participants.stream()
|
|
||||||
.filter(p -> safeBotUserIds.contains(p.getUserId()))
|
|
||||||
.toList();
|
|
||||||
if (!safeBotsInRound.isEmpty()) {
|
|
||||||
List<GameRoundParticipant> lowBalanceSafeBots = new ArrayList<>();
|
|
||||||
for (GameRoundParticipant p : safeBotsInRound) {
|
|
||||||
UserB userB = userBRepository.findById(p.getUserId()).orElse(null);
|
|
||||||
if (userB != null && userB.getBalanceA() != null && userB.getBalanceA() < SAFE_BOT_BALANCE_THRESHOLD) {
|
|
||||||
lowBalanceSafeBots.add(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!lowBalanceSafeBots.isEmpty()) {
|
|
||||||
GameRoundParticipant chosen = lowBalanceSafeBots.get(new Random().nextInt(lowBalanceSafeBots.size()));
|
|
||||||
log.debug("Safe bot winner override: userId={}, balance below threshold", chosen.getUserId());
|
|
||||||
return Optional.of(chosen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Flexible bot: with probability win_rate that bot wins; remaining probability = normal weighted random
|
|
||||||
List<GameRoundParticipant> flexBotsInRound = participants.stream()
|
|
||||||
.filter(p -> flexibleWinRates.containsKey(p.getUserId()))
|
|
||||||
.toList();
|
|
||||||
if (flexBotsInRound.isEmpty()) return Optional.empty();
|
|
||||||
|
|
||||||
double roll = new Random().nextDouble();
|
|
||||||
double cumulative = 0;
|
|
||||||
for (GameRoundParticipant p : flexBotsInRound) {
|
|
||||||
double rate = flexibleWinRates.get(p.getUserId());
|
|
||||||
cumulative += rate;
|
|
||||||
if (roll < cumulative) {
|
|
||||||
log.debug("Flexible bot winner override: userId={}, winRate={}", p.getUserId(), rate);
|
|
||||||
return Optional.of(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// roll >= cumulative: fall through to normal (don't return empty here - we already have normal logic in caller)
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Integer> getSafeBotUserIds() {
|
|
||||||
return safeBotUserRepository.findAllByOrderByUserIdAsc().stream()
|
|
||||||
.map(SafeBotUser::getUserId)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<Integer, Double> getFlexibleBotWinRates() {
|
|
||||||
Map<Integer, Double> map = new HashMap<>();
|
|
||||||
for (FlexibleBotConfig c : flexibleBotConfigRepository.findAllByOrderByUserIdAsc()) {
|
|
||||||
if (c.getWinRate() != null) {
|
|
||||||
map.put(c.getUserId(), c.getWinRate().doubleValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public BotConfigDto getConfig() {
|
|
||||||
List<Integer> safeBotUserIds = safeBotUserRepository.findAllByOrderByUserIdAsc().stream()
|
|
||||||
.map(SafeBotUser::getUserId)
|
|
||||||
.toList();
|
|
||||||
List<FlexibleBotEntryDto> flexibleBots = flexibleBotConfigRepository.findAllByOrderByUserIdAsc().stream()
|
|
||||||
.map(c -> new FlexibleBotEntryDto(c.getUserId(), c.getWinRate() != null ? c.getWinRate().doubleValue() : 0))
|
|
||||||
.toList();
|
|
||||||
return new BotConfigDto(safeBotUserIds, flexibleBots);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void setSafeBotUserIds(List<Integer> userIds) {
|
|
||||||
safeBotUserRepository.deleteAll();
|
|
||||||
if (userIds != null) {
|
|
||||||
for (Integer id : userIds) {
|
|
||||||
if (id != null) safeBotUserRepository.save(SafeBotUser.builder().userId(id).build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void setFlexibleBots(List<FlexibleBotEntryDto> entries) {
|
|
||||||
flexibleBotConfigRepository.deleteAll();
|
|
||||||
if (entries != null) {
|
|
||||||
for (FlexibleBotEntryDto e : entries) {
|
|
||||||
if (e != null && e.userId() != null && e.winRate() != null) {
|
|
||||||
double r = Math.max(0, Math.min(1, e.winRate()));
|
|
||||||
flexibleBotConfigRepository.save(FlexibleBotConfig.builder()
|
|
||||||
.userId(e.userId())
|
|
||||||
.winRate(BigDecimal.valueOf(r))
|
|
||||||
.updatedAt(Instant.now())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record BotConfigDto(List<Integer> safeBotUserIds, List<FlexibleBotEntryDto> flexibleBots) {}
|
|
||||||
public record FlexibleBotEntryDto(Integer userId, Double winRate) {}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.lottery.lottery.service;
|
package com.lottery.lottery.service;
|
||||||
|
|
||||||
import com.lottery.lottery.repository.GameRoundParticipantRepository;
|
|
||||||
import com.lottery.lottery.repository.TransactionRepository;
|
import com.lottery.lottery.repository.TransactionRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
import com.lottery.lottery.dto.GameHistoryEntryDto;
|
|
||||||
import com.lottery.lottery.model.Transaction;
|
|
||||||
import com.lottery.lottery.repository.TransactionRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for retrieving game history for users.
|
|
||||||
* Fetches WIN transactions from the last 30 days.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class GameHistoryService {
|
|
||||||
|
|
||||||
private final TransactionRepository transactionRepository;
|
|
||||||
private final LocalizationService localizationService;
|
|
||||||
private static final int PAGE_SIZE = 50;
|
|
||||||
private static final int DAYS_TO_FETCH = 30;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets WIN transactions for a user from the last 30 days with pagination.
|
|
||||||
*
|
|
||||||
* @param userId User ID
|
|
||||||
* @param page Page number (0-indexed)
|
|
||||||
* @param timezone Optional timezone (e.g., "Europe/London"). If null, uses UTC.
|
|
||||||
* @param languageCode Optional language code for date formatting (e.g., "EN", "RU"). If null, uses "EN".
|
|
||||||
* @return Page of game history entries with amount and date
|
|
||||||
*/
|
|
||||||
public Page<GameHistoryEntryDto> getUserGameHistory(Integer userId, int page, String timezone, String languageCode) {
|
|
||||||
Instant thirtyDaysAgo = Instant.now().minus(DAYS_TO_FETCH, ChronoUnit.DAYS);
|
|
||||||
Pageable pageable = PageRequest.of(page, PAGE_SIZE);
|
|
||||||
|
|
||||||
// Fetch WIN transactions from the last 30 days
|
|
||||||
Page<Transaction> transactions = transactionRepository.findByUserIdAndTypeAndCreatedAtAfterOrderByCreatedAtDesc(
|
|
||||||
userId, Transaction.TransactionType.WIN, thirtyDaysAgo, pageable);
|
|
||||||
|
|
||||||
// Determine timezone to use
|
|
||||||
ZoneId zoneId;
|
|
||||||
try {
|
|
||||||
zoneId = (timezone != null && !timezone.trim().isEmpty())
|
|
||||||
? ZoneId.of(timezone)
|
|
||||||
: ZoneId.of("UTC");
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Invalid timezone, fallback to UTC
|
|
||||||
zoneId = ZoneId.of("UTC");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get localized "at" word
|
|
||||||
String atWord = localizationService.getMessage("dateTime.at", languageCode);
|
|
||||||
if (atWord == null || atWord.isEmpty()) {
|
|
||||||
atWord = "at"; // Fallback to English
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create formatter with localized "at" word
|
|
||||||
final ZoneId finalZoneId = zoneId;
|
|
||||||
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM '" + atWord + "' HH:mm")
|
|
||||||
.withZone(finalZoneId);
|
|
||||||
|
|
||||||
return transactions.map(transaction -> {
|
|
||||||
// Format date as dd.MM at HH:mm (with localized "at" word)
|
|
||||||
String date = formatter.format(transaction.getCreatedAt());
|
|
||||||
|
|
||||||
// Amount is the total payout (already positive in WIN transactions)
|
|
||||||
return GameHistoryEntryDto.builder()
|
|
||||||
.amount(transaction.getAmount())
|
|
||||||
.date(date)
|
|
||||||
.build();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,223 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
import com.lottery.lottery.exception.BetDecisionException;
|
|
||||||
import com.lottery.lottery.exception.GameException;
|
|
||||||
import com.lottery.lottery.model.GamePhase;
|
|
||||||
import com.lottery.lottery.model.GameRound;
|
|
||||||
import com.lottery.lottery.model.GameRoundParticipant;
|
|
||||||
import com.lottery.lottery.model.GameRoom;
|
|
||||||
import com.lottery.lottery.model.LotteryBotConfig;
|
|
||||||
import com.lottery.lottery.repository.GameRoomRepository;
|
|
||||||
import com.lottery.lottery.repository.GameRoundParticipantRepository;
|
|
||||||
import com.lottery.lottery.repository.GameRoundRepository;
|
|
||||||
import com.lottery.lottery.repository.LotteryBotConfigRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalTime;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scheduler that registers lottery bots into joinable rounds based on
|
|
||||||
* lottery_bot_configs (time window, room flags, active). Joins only when:
|
|
||||||
* - round has no participants for at least 1 minute, or
|
|
||||||
* - round has exactly one participant who has been waiting longer than 10 seconds.
|
|
||||||
* Does not join when 2+ participants. Uses BetDecisionService (persona + streak) for bet amount.
|
|
||||||
* Does not use /remotebet.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class LotteryBotSchedulerService {
|
|
||||||
|
|
||||||
private static final long TICKETS_TO_BIGINT = 1_000_000L;
|
|
||||||
/** Round empty (0 participants) for at least this long before a bot may join. */
|
|
||||||
private static final long EMPTY_ROOM_THRESHOLD_SECONDS = 1L;
|
|
||||||
/** Single participant must be waiting at least this long before a bot may join. */
|
|
||||||
private static final long ONE_PARTICIPANT_WAIT_THRESHOLD_SECONDS = 3L;
|
|
||||||
|
|
||||||
private static final int BOT_HISTORY_SIZE = 10;
|
|
||||||
|
|
||||||
private final GameRoomRepository gameRoomRepository;
|
|
||||||
private final GameRoundRepository gameRoundRepository;
|
|
||||||
private final GameRoundParticipantRepository participantRepository;
|
|
||||||
private final LotteryBotConfigRepository lotteryBotConfigRepository;
|
|
||||||
private final GameRoomService gameRoomService;
|
|
||||||
private final BetDecisionService betDecisionService;
|
|
||||||
private final BotBetHistoryService botBetHistoryService;
|
|
||||||
private final FeatureSwitchService featureSwitchService;
|
|
||||||
private final ConfigurationService configurationService;
|
|
||||||
|
|
||||||
/** Per room: first time we observed no active round (for EMPTY_ROOM_THRESHOLD). Cleared when room has an active round again. */
|
|
||||||
private final Map<Integer, Instant> roomFirstSeenNoRound = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every 15 seconds: for each room, if joinable and round state allows (0 participants >= 1 min, or 1 participant >= 10 sec), register eligible bots.
|
|
||||||
* Controlled by feature switch {@code lottery_bot_scheduler_enabled} (default on).
|
|
||||||
*/
|
|
||||||
@Scheduled(fixedDelayString = "${app.lottery-bot.schedule-fixed-delay-ms:5000}")
|
|
||||||
public void registerBotsForJoinableRooms() {
|
|
||||||
boolean featureOn = featureSwitchService.isLotteryBotSchedulerEnabled();
|
|
||||||
List<LotteryBotConfig> activeConfigs = lotteryBotConfigRepository.findAllByActiveTrue();
|
|
||||||
|
|
||||||
if (!featureOn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activeConfigs.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalTime nowUtc = LocalTime.now(ZoneOffset.UTC);
|
|
||||||
|
|
||||||
for (int roomNumber = 1; roomNumber <= 3; roomNumber++) {
|
|
||||||
Optional<GameRoom> roomOpt = gameRoomRepository.findByRoomNumber(roomNumber);
|
|
||||||
if (roomOpt.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
GameRoom room = roomOpt.get();
|
|
||||||
if (room.getCurrentPhase() != GamePhase.WAITING && room.getCurrentPhase() != GamePhase.COUNTDOWN) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 0 participants: no round exists yet (round is created on first join). Check no active round in WAITING/COUNTDOWN/SPINNING.
|
|
||||||
// 1 participant: we need an active round in WAITING phase only (not COUNTDOWN).
|
|
||||||
List<GameRound> roundsActive = gameRoundRepository.findMostRecentActiveRoundsByRoomId(
|
|
||||||
room.getId(), List.of(GamePhase.WAITING, GamePhase.COUNTDOWN, GamePhase.SPINNING), PageRequest.of(0, 1));
|
|
||||||
|
|
||||||
GameRound round = null;
|
|
||||||
List<GameRoundParticipant> participants = List.of();
|
|
||||||
int participantCount = 0;
|
|
||||||
boolean mayJoin = false;
|
|
||||||
|
|
||||||
if (roundsActive.isEmpty()) {
|
|
||||||
// No active round → 0 participants. Enforce EMPTY_ROOM_THRESHOLD: only join after room has been empty for that long.
|
|
||||||
participantCount = 0;
|
|
||||||
Instant now = Instant.now();
|
|
||||||
Instant firstSeenNoRound = roomFirstSeenNoRound.computeIfAbsent(roomNumber, k -> now);
|
|
||||||
mayJoin = !now.isBefore(firstSeenNoRound.plusSeconds(EMPTY_ROOM_THRESHOLD_SECONDS));
|
|
||||||
if (!mayJoin) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
roomFirstSeenNoRound.remove(roomNumber); // room has an active round again, clear so next "no round" starts 30s from scratch
|
|
||||||
round = roundsActive.get(0);
|
|
||||||
participants = participantRepository.findByRoundId(round.getId());
|
|
||||||
participantCount = participants.size();
|
|
||||||
|
|
||||||
int maxParticipantsBeforeBotJoin = configurationService.getMaxParticipantsBeforeBotJoin();
|
|
||||||
if (participantCount > maxParticipantsBeforeBotJoin) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Instant now = Instant.now();
|
|
||||||
if (participantCount == 0) {
|
|
||||||
mayJoin = round.getStartedAt() != null
|
|
||||||
&& round.getStartedAt().plusSeconds(EMPTY_ROOM_THRESHOLD_SECONDS).isBefore(now);
|
|
||||||
} else {
|
|
||||||
// 1..N participants: only join if round is in WAITING (not COUNTDOWN) and oldest participant waited long enough
|
|
||||||
if (round.getPhase() != GamePhase.WAITING) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Instant oldestJoined = participants.stream()
|
|
||||||
.map(GameRoundParticipant::getJoinedAt)
|
|
||||||
.min(Instant::compareTo)
|
|
||||||
.orElse(Instant.EPOCH);
|
|
||||||
mayJoin = oldestJoined.plusSeconds(ONE_PARTICIPANT_WAIT_THRESHOLD_SECONDS).isBefore(now);
|
|
||||||
}
|
|
||||||
if (!mayJoin) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffle so we don't always try the same bot first (e.g. by config id)
|
|
||||||
List<LotteryBotConfig> configsToTry = new ArrayList<>(activeConfigs);
|
|
||||||
Collections.shuffle(configsToTry);
|
|
||||||
|
|
||||||
for (LotteryBotConfig config : configsToTry) {
|
|
||||||
if (!isRoomEnabledForConfig(config, roomNumber)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!isCurrentTimeInWindow(nowUtc, config.getTimeUtcStart(), config.getTimeUtcEnd())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int userId = config.getUserId();
|
|
||||||
if (gameRoomService.getCurrentUserBetInRoom(userId, roomNumber) > 0L) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameRoomService.BetLimits limits = GameRoomService.getBetLimitsForRoom(roomNumber);
|
|
||||||
long roomMinTickets = limits.minBet() / TICKETS_TO_BIGINT;
|
|
||||||
long roomMaxTickets = limits.maxBet() / TICKETS_TO_BIGINT;
|
|
||||||
long botMinTickets = config.getBetMin() != null ? config.getBetMin() / TICKETS_TO_BIGINT : roomMinTickets;
|
|
||||||
long botMaxTickets = config.getBetMax() != null ? config.getBetMax() / TICKETS_TO_BIGINT : roomMaxTickets;
|
|
||||||
long currentRoundTotalBetTickets = participants.stream()
|
|
||||||
.mapToLong(p -> p.getBet() != null ? p.getBet() / TICKETS_TO_BIGINT : 0L)
|
|
||||||
.sum();
|
|
||||||
var history = botBetHistoryService.getLastBetsAndResults(userId, BOT_HISTORY_SIZE);
|
|
||||||
|
|
||||||
try {
|
|
||||||
long tickets = betDecisionService.decideBetAmountTickets(
|
|
||||||
BotBetContext.builder()
|
|
||||||
.roomNumber(roomNumber)
|
|
||||||
.roundId(round != null ? round.getId() : null)
|
|
||||||
.participantCount(participantCount)
|
|
||||||
.config(config)
|
|
||||||
.roomMinTickets(roomMinTickets)
|
|
||||||
.roomMaxTickets(roomMaxTickets)
|
|
||||||
.botMinTickets(botMinTickets)
|
|
||||||
.botMaxTickets(botMaxTickets)
|
|
||||||
.currentRoundTotalBetTickets(currentRoundTotalBetTickets)
|
|
||||||
.lastBets10(history.lastBets())
|
|
||||||
.lastResults10(history.lastResults())
|
|
||||||
.build());
|
|
||||||
long betBigint = Math.max(1L, tickets) * TICKETS_TO_BIGINT;
|
|
||||||
betBigint = Math.max(limits.minBet(), Math.min(limits.maxBet(), betBigint));
|
|
||||||
|
|
||||||
gameRoomService.joinRoundWithResult(userId, roomNumber, betBigint, true);
|
|
||||||
log.info("Lottery bot registered: userId={}, room={}, betTickets={}", userId, roomNumber, tickets);
|
|
||||||
// Only one bot per room per run; next run will see updated participant count and enforce 7s wait for second bot
|
|
||||||
break;
|
|
||||||
} catch (BetDecisionException e) {
|
|
||||||
log.warn("Bot not registered (bet decision failed): userId={}, room={}, reason={}", userId, roomNumber, e.getMessage());
|
|
||||||
} catch (GameException e) {
|
|
||||||
log.warn("Bot join skipped: userId={}, room={}, reason={}", userId, roomNumber, e.getUserMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Bot join failed: userId={}, room={}", userId, roomNumber, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isRoomEnabledForConfig(LotteryBotConfig config, int roomNumber) {
|
|
||||||
return switch (roomNumber) {
|
|
||||||
case 1 -> Boolean.TRUE.equals(config.getRoom1());
|
|
||||||
case 2 -> Boolean.TRUE.equals(config.getRoom2());
|
|
||||||
case 3 -> Boolean.TRUE.equals(config.getRoom3());
|
|
||||||
default -> false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if current UTC time is within [start, end].
|
|
||||||
* Handles overnight window: e.g. start 22:00, end 06:00 → true if now >= 22:00 or now <= 06:00.
|
|
||||||
*/
|
|
||||||
private static boolean isCurrentTimeInWindow(LocalTime now, LocalTime start, LocalTime end) {
|
|
||||||
if (start == null || end == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!start.isAfter(end)) {
|
|
||||||
return !now.isBefore(start) && !now.isAfter(end);
|
|
||||||
}
|
|
||||||
return !now.isBefore(start) || !now.isAfter(end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -230,8 +230,6 @@ public class PaymentService {
|
|||||||
// Update deposit statistics
|
// Update deposit statistics
|
||||||
userB.setDepositTotal(userB.getDepositTotal() + ticketsAmount);
|
userB.setDepositTotal(userB.getDepositTotal() + ticketsAmount);
|
||||||
userB.setDepositCount(userB.getDepositCount() + 1);
|
userB.setDepositCount(userB.getDepositCount() + 1);
|
||||||
// Reset total winnings since last deposit (withdrawal limit is based on this)
|
|
||||||
userB.setTotalWinAfterDeposit(0L);
|
|
||||||
|
|
||||||
userBRepository.save(userB);
|
userBRepository.save(userB);
|
||||||
|
|
||||||
@@ -378,7 +376,6 @@ public class PaymentService {
|
|||||||
userB.setBalanceA(userB.getBalanceA() + ticketsAmount);
|
userB.setBalanceA(userB.getBalanceA() + ticketsAmount);
|
||||||
userB.setDepositTotal(userB.getDepositTotal() + ticketsAmount);
|
userB.setDepositTotal(userB.getDepositTotal() + ticketsAmount);
|
||||||
userB.setDepositCount(userB.getDepositCount() + 1);
|
userB.setDepositCount(userB.getDepositCount() + 1);
|
||||||
userB.setTotalWinAfterDeposit(0L);
|
|
||||||
userBRepository.save(userB);
|
userBRepository.save(userB);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -135,13 +135,6 @@ public class PayoutService {
|
|||||||
throw new IllegalArgumentException(localizationService.getMessage("payout.error.invalidPayoutType"));
|
throw new IllegalArgumentException(localizationService.getMessage("payout.error.invalidPayoutType"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Withdrawal cannot exceed total winnings since last deposit
|
|
||||||
long maxWinAfterDeposit = userB.getTotalWinAfterDeposit() != null ? userB.getTotalWinAfterDeposit() : 0L;
|
|
||||||
if (payout.getTotal() > maxWinAfterDeposit) {
|
|
||||||
long maxTickets = maxWinAfterDeposit / 1_000_000L;
|
|
||||||
throw new IllegalArgumentException(localizationService.getMessage("payout.error.withdrawExceedsWinAfterDeposit", String.valueOf(maxTickets)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate tickets amount and user balance
|
// Validate tickets amount and user balance
|
||||||
validateTicketsAmount(userId, payout.getTotal());
|
validateTicketsAmount(userId, payout.getTotal());
|
||||||
|
|
||||||
@@ -176,12 +169,6 @@ public class PayoutService {
|
|||||||
validateTicketsAmount(userId, total);
|
validateTicketsAmount(userId, total);
|
||||||
validateCryptoWithdrawalMaxTwoDecimals(total);
|
validateCryptoWithdrawalMaxTwoDecimals(total);
|
||||||
|
|
||||||
long maxWinAfterDeposit = userB.getTotalWinAfterDeposit() != null ? userB.getTotalWinAfterDeposit() : 0L;
|
|
||||||
if (total > maxWinAfterDeposit) {
|
|
||||||
long maxTickets = maxWinAfterDeposit / 1_000_000L;
|
|
||||||
throw new IllegalArgumentException(localizationService.getMessage("payout.error.withdrawExceedsWinAfterDeposit", String.valueOf(maxTickets)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payoutRepository.existsByUserIdAndStatus(userId, Payout.PayoutStatus.PROCESSING)) {
|
if (payoutRepository.existsByUserIdAndStatus(userId, Payout.PayoutStatus.PROCESSING)) {
|
||||||
throw new IllegalArgumentException(localizationService.getMessage("payout.error.withdrawalInProgress"));
|
throw new IllegalArgumentException(localizationService.getMessage("payout.error.withdrawalInProgress"));
|
||||||
}
|
}
|
||||||
@@ -199,11 +186,6 @@ public class PayoutService {
|
|||||||
throw new IllegalArgumentException(localizationService.getMessage("payout.error.insufficientBalanceDetailed",
|
throw new IllegalArgumentException(localizationService.getMessage("payout.error.insufficientBalanceDetailed",
|
||||||
String.valueOf(userB.getBalanceA() / 1_000_000.0), String.valueOf(total / 1_000_000.0)));
|
String.valueOf(userB.getBalanceA() / 1_000_000.0), String.valueOf(total / 1_000_000.0)));
|
||||||
}
|
}
|
||||||
long maxWin = userB.getTotalWinAfterDeposit() != null ? userB.getTotalWinAfterDeposit() : 0L;
|
|
||||||
if (total > maxWin) {
|
|
||||||
long maxTickets = maxWin / 1_000_000L;
|
|
||||||
throw new IllegalArgumentException(localizationService.getMessage("payout.error.withdrawExceedsWinAfterDeposit", String.valueOf(maxTickets)));
|
|
||||||
}
|
|
||||||
|
|
||||||
double amountUsd = total / 1_000_000_000.0;
|
double amountUsd = total / 1_000_000_000.0;
|
||||||
boolean noWithdrawalsYet = (userB.getWithdrawCount() != null ? userB.getWithdrawCount() : 0) == 0;
|
boolean noWithdrawalsYet = (userB.getWithdrawCount() != null ? userB.getWithdrawCount() : 0) == 0;
|
||||||
@@ -514,7 +496,7 @@ public class PayoutService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies balance and totalWinAfterDeposit deduction to an already-loaded (and locked) UserB.
|
* Applies balance deduction to an already-loaded (and locked) UserB.
|
||||||
* Caller must hold a pessimistic lock on the UserB row (e.g. from findByIdForUpdate).
|
* Caller must hold a pessimistic lock on the UserB row (e.g. from findByIdForUpdate).
|
||||||
*/
|
*/
|
||||||
private void applyDeductToUserB(UserB userB, Integer userId, Long total) {
|
private void applyDeductToUserB(UserB userB, Integer userId, Long total) {
|
||||||
@@ -522,8 +504,6 @@ public class PayoutService {
|
|||||||
throw new IllegalStateException(localizationService.getMessage("payout.error.insufficientBalance"));
|
throw new IllegalStateException(localizationService.getMessage("payout.error.insufficientBalance"));
|
||||||
}
|
}
|
||||||
userB.setBalanceA(userB.getBalanceA() - total);
|
userB.setBalanceA(userB.getBalanceA() - total);
|
||||||
long currentWinAfterDeposit = userB.getTotalWinAfterDeposit() != null ? userB.getTotalWinAfterDeposit() : 0L;
|
|
||||||
userB.setTotalWinAfterDeposit(Math.max(0L, currentWinAfterDeposit - total));
|
|
||||||
userBRepository.save(userB);
|
userBRepository.save(userB);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
import com.lottery.lottery.exception.BetDecisionException;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bet decision using persona + loss streak and zone logic (no external API).
|
|
||||||
* Zones are defined as percentages of the room/bot range [min, max]; actual ticket
|
|
||||||
* bounds are computed from (minPct, maxPct) so the same behaviour applies for any range.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class PersonaBetDecisionService implements BetDecisionService {
|
|
||||||
|
|
||||||
private static final String LOSS = "L";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long decideBetAmountTickets(BotBetContext context) {
|
|
||||||
long botMin = context.getBotMinTickets();
|
|
||||||
long botMax = context.getBotMaxTickets();
|
|
||||||
if (botMin <= 0 || botMax < botMin) {
|
|
||||||
throw new BetDecisionException("Invalid bot range [" + botMin + ", " + botMax + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
String persona = context.getConfig() != null && context.getConfig().getPersona() != null
|
|
||||||
? context.getConfig().getPersona().trim().toLowerCase() : "balanced";
|
|
||||||
List<String> results = context.getLastResults10();
|
|
||||||
if (results == null) results = List.of();
|
|
||||||
|
|
||||||
int streak = countConsecutiveLossesFromEnd(results);
|
|
||||||
long[] zone = getZoneTicketsForPersonaAndStreak(persona, streak, botMin, botMax);
|
|
||||||
long zoneMin = zone[0];
|
|
||||||
long zoneMax = zone[1];
|
|
||||||
|
|
||||||
// Clamp zone to bot range
|
|
||||||
long clampedMin = Math.max(zoneMin, botMin);
|
|
||||||
long clampedMax = Math.min(zoneMax, botMax);
|
|
||||||
if (clampedMin > clampedMax) {
|
|
||||||
clampedMin = botMin;
|
|
||||||
clampedMax = botMax;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pick random in [clampedMin, clampedMax] for variety
|
|
||||||
long rawBet = clampedMin == clampedMax ? clampedMin
|
|
||||||
: clampedMin + ThreadLocalRandom.current().nextLong(clampedMax - clampedMin + 1);
|
|
||||||
|
|
||||||
long step = getStep(botMin);
|
|
||||||
long bet = roundToStep(rawBet, botMin, botMax, step);
|
|
||||||
|
|
||||||
log.debug("Persona bet decision: persona={}, streak={}, zone=[{}, {}] -> {} tickets", persona, streak, zoneMin, zoneMax, bet);
|
|
||||||
return bet;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Step 1 if min < 10, step 10 if 10 <= min < 1000, step 100 if min >= 1000. */
|
|
||||||
private static long getStep(long botMin) {
|
|
||||||
if (botMin >= 1000) return 100;
|
|
||||||
if (botMin >= 10) return 10;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Count consecutive L from the end of last results (oldest → newest). */
|
|
||||||
private static int countConsecutiveLossesFromEnd(List<String> results) {
|
|
||||||
int count = 0;
|
|
||||||
for (int i = results.size() - 1; i >= 0; i--) {
|
|
||||||
if (LOSS.equals(results.get(i))) count++;
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zone rule: when loss streak >= streakThreshold, use [minPct, maxPct] of range (0 = min, 100 = max).
|
|
||||||
* Rules are evaluated in descending streak order (highest threshold first).
|
|
||||||
*/
|
|
||||||
private static final class ZoneRule {
|
|
||||||
final int streakThreshold;
|
|
||||||
final double minPct;
|
|
||||||
final double maxPct;
|
|
||||||
|
|
||||||
ZoneRule(int streakThreshold, double minPct, double maxPct) {
|
|
||||||
this.streakThreshold = streakThreshold;
|
|
||||||
this.minPct = minPct;
|
|
||||||
this.maxPct = maxPct;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conservative: usually 1–10%, streak 5 → 19–24%, streak 7 → 39–50%
|
|
||||||
private static final List<ZoneRule> CONSERVATIVE_RULES = List.of(
|
|
||||||
new ZoneRule(7, 45, 68),
|
|
||||||
new ZoneRule(5, 17, 28),
|
|
||||||
new ZoneRule(0, 1, 10)
|
|
||||||
);
|
|
||||||
// Balanced: usually 5–15%, streak 3 → 29–39%, streak 5 → 59–69%
|
|
||||||
private static final List<ZoneRule> BALANCED_RULES = List.of(
|
|
||||||
new ZoneRule(5, 64, 79),
|
|
||||||
new ZoneRule(3, 25, 37),
|
|
||||||
new ZoneRule(0, 2, 12)
|
|
||||||
);
|
|
||||||
// Aggressive: usually 15–25%, streak 2 → 39–50%, streak 3 → 79–100%
|
|
||||||
private static final List<ZoneRule> AGGRESSIVE_RULES = List.of(
|
|
||||||
new ZoneRule(3, 79, 100),
|
|
||||||
new ZoneRule(2, 33, 42),
|
|
||||||
new ZoneRule(0, 5, 15)
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Returns [zoneMinTickets, zoneMaxTickets] from percentage-of-range rules for persona and streak. */
|
|
||||||
private static long[] getZoneTicketsForPersonaAndStreak(String persona, int streak, long rangeMin, long rangeMax) {
|
|
||||||
List<ZoneRule> rules = switch (persona) {
|
|
||||||
case "conservative" -> CONSERVATIVE_RULES;
|
|
||||||
case "aggressive" -> AGGRESSIVE_RULES;
|
|
||||||
default -> BALANCED_RULES;
|
|
||||||
};
|
|
||||||
ZoneRule rule = rules.stream()
|
|
||||||
.filter(r -> streak >= r.streakThreshold)
|
|
||||||
.findFirst()
|
|
||||||
.orElse(rules.get(rules.size() - 1));
|
|
||||||
long range = rangeMax - rangeMin;
|
|
||||||
long zoneMin = rangeMin + Math.round(range * rule.minPct / 100.0);
|
|
||||||
long zoneMax = rangeMin + Math.round(range * rule.maxPct / 100.0);
|
|
||||||
zoneMin = Math.max(rangeMin, Math.min(zoneMin, rangeMax));
|
|
||||||
zoneMax = Math.max(rangeMin, Math.min(zoneMax, rangeMax));
|
|
||||||
if (zoneMin > zoneMax) zoneMin = zoneMax;
|
|
||||||
return new long[]{zoneMin, zoneMax};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Round value to nearest valid step in [min, max]. */
|
|
||||||
private static long roundToStep(long value, long min, long max, long step) {
|
|
||||||
long offset = value - min;
|
|
||||||
long steps = Math.round((double) offset / step);
|
|
||||||
long rounded = min + steps * step;
|
|
||||||
return Math.max(min, Math.min(max, rounded));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
import com.lottery.lottery.model.Transaction;
|
|
||||||
import com.lottery.lottery.model.UserB;
|
|
||||||
import com.lottery.lottery.model.UserD;
|
|
||||||
import com.lottery.lottery.repository.TransactionRepository;
|
|
||||||
import com.lottery.lottery.repository.UserBRepository;
|
|
||||||
import com.lottery.lottery.repository.UserDRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for handling referral commission logic.
|
|
||||||
* Processes commissions for referers when their referrals win or lose game rounds.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ReferralCommissionService {
|
|
||||||
|
|
||||||
private final UserDRepository userDRepository;
|
|
||||||
private final UserBRepository userBRepository;
|
|
||||||
private final TransactionRepository transactionRepository;
|
|
||||||
|
|
||||||
// Commission rates (as percentages)
|
|
||||||
private static final double WIN_PROFIT_COMMISSION_RATE = 0.01; // 1% of net profit for winners (all levels)
|
|
||||||
// Loss commission rates per referrer level
|
|
||||||
private static final double LOSS_COMMISSION_RATE_LEVEL1 = 0.04; // 4% of loss for referrer 1
|
|
||||||
private static final double LOSS_COMMISSION_RATE_LEVEL2 = 0.02; // 2% of loss for referrer 2
|
|
||||||
private static final double LOSS_COMMISSION_RATE_LEVEL3 = 0.01; // 1% of loss for referrer 3
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes referral commissions for a user who won a round.
|
|
||||||
*
|
|
||||||
* @param userId The user who won
|
|
||||||
* @param userBet The user's bet amount (in bigint format)
|
|
||||||
* @param totalBet The total bet of the round (in bigint format)
|
|
||||||
* @param houseCommission The house commission amount (20% of totalBet - userBet, in bigint format)
|
|
||||||
* @return Set of referer user IDs who received commissions (for balance update notifications)
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public Set<Integer> processWinnerCommissions(Integer userId, Long userBet, Long totalBet, Long houseCommission) {
|
|
||||||
Set<Integer> refererIds = new HashSet<>();
|
|
||||||
// Calculate user's net profit: (totalBet - houseCommission) - userBet
|
|
||||||
// This is the actual profit after the house takes its 20% commission
|
|
||||||
long userProfit = totalBet - houseCommission - userBet;
|
|
||||||
|
|
||||||
if (userProfit <= 0) {
|
|
||||||
log.debug("No profit to distribute for winner userId={}, userProfit={}", userId, userProfit);
|
|
||||||
return refererIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate referral commission amount: 1% of user's net profit
|
|
||||||
long commissionAmount = (long) (userProfit * WIN_PROFIT_COMMISSION_RATE);
|
|
||||||
|
|
||||||
if (commissionAmount <= 0) {
|
|
||||||
log.debug("Commission amount too small for winner userId={}, commissionAmount={}", userId, commissionAmount);
|
|
||||||
return refererIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Processing winner commissions: userId={}, userProfit={}, houseCommission={}, commissionAmount={}",
|
|
||||||
userId, userProfit, houseCommission, commissionAmount);
|
|
||||||
|
|
||||||
// Get user's referral chain
|
|
||||||
UserD userD = userDRepository.findById(userId).orElse(null);
|
|
||||||
if (userD == null) {
|
|
||||||
log.warn("UserD not found for userId={}, skipping referral commissions", userId);
|
|
||||||
return refererIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process commissions for referer_1, referer_2, referer_3
|
|
||||||
Integer referer1 = processRefererCommission(userD.getRefererId1(), commissionAmount, 1, userId, true);
|
|
||||||
if (referer1 != null) refererIds.add(referer1);
|
|
||||||
|
|
||||||
Integer referer2 = processRefererCommission(userD.getRefererId2(), commissionAmount, 2, userId, true);
|
|
||||||
if (referer2 != null) refererIds.add(referer2);
|
|
||||||
|
|
||||||
Integer referer3 = processRefererCommission(userD.getRefererId3(), commissionAmount, 3, userId, true);
|
|
||||||
if (referer3 != null) refererIds.add(referer3);
|
|
||||||
|
|
||||||
return refererIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes referral commissions for a user who lost a round.
|
|
||||||
*
|
|
||||||
* @param userId The user who lost
|
|
||||||
* @param userBet The user's bet amount (in bigint format)
|
|
||||||
* @return Set of referer user IDs who received commissions (for balance update notifications)
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public Set<Integer> processLoserCommissions(Integer userId, Long userBet) {
|
|
||||||
Set<Integer> refererIds = new HashSet<>();
|
|
||||||
if (userBet <= 0) {
|
|
||||||
log.debug("No bet to process commissions for loser userId={}, userBet={}", userId, userBet);
|
|
||||||
return refererIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Processing loser commissions: userId={}, userBet={}, rates: level1={}%, level2={}%, level3={}%",
|
|
||||||
userId, userBet, LOSS_COMMISSION_RATE_LEVEL1 * 100, LOSS_COMMISSION_RATE_LEVEL2 * 100, LOSS_COMMISSION_RATE_LEVEL3 * 100);
|
|
||||||
|
|
||||||
// Get user's referral chain
|
|
||||||
UserD userD = userDRepository.findById(userId).orElse(null);
|
|
||||||
if (userD == null) {
|
|
||||||
log.warn("UserD not found for userId={}, skipping referral commissions", userId);
|
|
||||||
return refererIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process commissions for referer_1, referer_2, referer_3 with different rates
|
|
||||||
// Referrer 1: 4% of user's bet
|
|
||||||
long commissionAmount1 = (long) (userBet * LOSS_COMMISSION_RATE_LEVEL1);
|
|
||||||
if (commissionAmount1 > 0) {
|
|
||||||
Integer referer1 = processRefererCommission(userD.getRefererId1(), commissionAmount1, 1, userId, false);
|
|
||||||
if (referer1 != null) refererIds.add(referer1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Referrer 2: 2% of user's bet
|
|
||||||
long commissionAmount2 = (long) (userBet * LOSS_COMMISSION_RATE_LEVEL2);
|
|
||||||
if (commissionAmount2 > 0) {
|
|
||||||
Integer referer2 = processRefererCommission(userD.getRefererId2(), commissionAmount2, 2, userId, false);
|
|
||||||
if (referer2 != null) refererIds.add(referer2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Referrer 3: 1% of user's bet
|
|
||||||
long commissionAmount3 = (long) (userBet * LOSS_COMMISSION_RATE_LEVEL3);
|
|
||||||
if (commissionAmount3 > 0) {
|
|
||||||
Integer referer3 = processRefererCommission(userD.getRefererId3(), commissionAmount3, 3, userId, false);
|
|
||||||
if (referer3 != null) refererIds.add(referer3);
|
|
||||||
}
|
|
||||||
|
|
||||||
return refererIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes commission for a single referer.
|
|
||||||
*
|
|
||||||
* @param refererId The referer's user ID (0 if no referer)
|
|
||||||
* @param commissionAmount The commission amount to award (in bigint format)
|
|
||||||
* @param refererLevel The referer level (1, 2, or 3)
|
|
||||||
* @param userId The user who triggered the commission
|
|
||||||
* @param isWinner Whether the user won (true) or lost (false)
|
|
||||||
* @return The referer ID if commission was processed, null otherwise
|
|
||||||
*/
|
|
||||||
private Integer processRefererCommission(Integer refererId, Long commissionAmount, int refererLevel,
|
|
||||||
Integer userId, boolean isWinner) {
|
|
||||||
// Skip if no referer
|
|
||||||
if (refererId == null || refererId <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get referer's UserB (balance) and UserD (referral stats)
|
|
||||||
UserB refererBalance = userBRepository.findById(refererId).orElse(null);
|
|
||||||
UserD refererD = userDRepository.findById(refererId).orElse(null);
|
|
||||||
|
|
||||||
if (refererBalance == null || refererD == null) {
|
|
||||||
log.warn("Referer not found: refererId={}, refererLevel={}, userId={}",
|
|
||||||
refererId, refererLevel, userId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Credit referer's balance
|
|
||||||
refererBalance.setBalanceA(refererBalance.getBalanceA() + commissionAmount);
|
|
||||||
userBRepository.save(refererBalance);
|
|
||||||
|
|
||||||
// Update referer's from_referals_X (they earned from their referral)
|
|
||||||
switch (refererLevel) {
|
|
||||||
case 1:
|
|
||||||
refererD.setFromReferals1(refererD.getFromReferals1() + commissionAmount);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
refererD.setFromReferals2(refererD.getFromReferals2() + commissionAmount);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
refererD.setFromReferals3(refererD.getFromReferals3() + commissionAmount);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
userDRepository.save(refererD);
|
|
||||||
|
|
||||||
// Update user's to_referer_X (they paid commission to their referer)
|
|
||||||
UserD userD = userDRepository.findById(userId).orElse(null);
|
|
||||||
if (userD != null) {
|
|
||||||
switch (refererLevel) {
|
|
||||||
case 1:
|
|
||||||
userD.setToReferer1(userD.getToReferer1() + commissionAmount);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
userD.setToReferer2(userD.getToReferer2() + commissionAmount);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
userD.setToReferer3(userD.getToReferer3() + commissionAmount);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
userDRepository.save(userD);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Commission processed: refererId={}, refererLevel={}, userId={}, " +
|
|
||||||
"commissionAmount={}, isWinner={}",
|
|
||||||
refererId, refererLevel, userId, commissionAmount, isWinner);
|
|
||||||
|
|
||||||
return refererId;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error processing commission for refererId={}, refererLevel={}, userId={}",
|
|
||||||
refererId, refererLevel, userId, e);
|
|
||||||
// Continue processing other referers even if one fails
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the next bet will be the user's 3rd bet.
|
|
||||||
*
|
|
||||||
* @param userId The user who is about to place a bet
|
|
||||||
* @return true if this will be the 3rd bet, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean willBeThirdBet(Integer userId) {
|
|
||||||
try {
|
|
||||||
// Get user's rounds_played count from db_users_b
|
|
||||||
UserB userB = userBRepository.findById(userId).orElse(null);
|
|
||||||
if (userB == null) {
|
|
||||||
log.warn("UserB not found for userId={}", userId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// If current rounds_played is 2, the next round will be the 3rd
|
|
||||||
return userB.getRoundsPlayed() == 2;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error checking rounds_played for userId={}", userId, e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gives a one-time bonus of 1 ticket to referrer 1 when user places their 3rd bet.
|
|
||||||
*
|
|
||||||
* @param userId The user who placed their 3rd bet
|
|
||||||
* @return The referrer 1 user ID if bonus was given, null otherwise
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public Integer giveThirdBetBonus(Integer userId) {
|
|
||||||
try {
|
|
||||||
// Get user's referral chain
|
|
||||||
UserD userD = userDRepository.findById(userId).orElse(null);
|
|
||||||
if (userD == null || userD.getRefererId1() == null || userD.getRefererId1() <= 0) {
|
|
||||||
log.debug("No referrer 1 for userId={}, skipping 3rd bet bonus", userId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Integer referer1Id = userD.getRefererId1();
|
|
||||||
|
|
||||||
// Get referrer's balance and referral stats
|
|
||||||
UserB refererBalance = userBRepository.findById(referer1Id).orElse(null);
|
|
||||||
UserD refererD = userDRepository.findById(referer1Id).orElse(null);
|
|
||||||
|
|
||||||
if (refererBalance == null || refererD == null) {
|
|
||||||
log.warn("Referrer 1 not found: refererId={}, userId={}", referer1Id, userId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give 1 ticket bonus (1,000,000 in bigint format)
|
|
||||||
long bonusAmount = 1_000_000L;
|
|
||||||
refererBalance.setBalanceA(refererBalance.getBalanceA() + bonusAmount);
|
|
||||||
userBRepository.save(refererBalance);
|
|
||||||
|
|
||||||
// Update referrer's from_referals_1 (they earned from their referral)
|
|
||||||
refererD.setFromReferals1(refererD.getFromReferals1() + bonusAmount);
|
|
||||||
userDRepository.save(refererD);
|
|
||||||
|
|
||||||
// Update user's to_referer_1 (they gave bonus to their referrer)
|
|
||||||
userD.setToReferer1(userD.getToReferer1() + bonusAmount);
|
|
||||||
userDRepository.save(userD);
|
|
||||||
|
|
||||||
log.info("3rd bet bonus given: userId={}, referer1Id={}, bonusAmount={}",
|
|
||||||
userId, referer1Id, bonusAmount);
|
|
||||||
|
|
||||||
return referer1Id;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error giving 3rd bet bonus for userId={}", userId, e);
|
|
||||||
// Don't throw - this is a bonus, shouldn't break the main flow
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
package com.lottery.lottery.service;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks room-level WebSocket connections.
|
|
||||||
* Tracks which users are connected to which rooms, regardless of round participation.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class RoomConnectionService {
|
|
||||||
|
|
||||||
// Callback to notify when room connections change (set by GameRoomService)
|
|
||||||
private BiConsumer<Integer, Integer> connectionChangeCallback;
|
|
||||||
|
|
||||||
// Track room connections: roomNumber -> userId -> Set of sessionIds
|
|
||||||
// This allows tracking multiple sessions per user (e.g., web + iOS)
|
|
||||||
private final Map<Integer, Map<Integer, Set<String>>> roomConnections = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
// Track session to user mapping: sessionId -> userId (for disconnect events when principal is lost)
|
|
||||||
private final Map<String, Integer> sessionToUser = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets callback to be notified when room connections change.
|
|
||||||
* Called by GameRoomService during initialization.
|
|
||||||
*/
|
|
||||||
public void setConnectionChangeCallback(BiConsumer<Integer, Integer> callback) {
|
|
||||||
this.connectionChangeCallback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a session-to-user mapping.
|
|
||||||
* Called when user connects to track sessions for disconnect events.
|
|
||||||
*/
|
|
||||||
public void registerSession(String sessionId, Integer userId) {
|
|
||||||
sessionToUser.put(sessionId, userId);
|
|
||||||
log.debug("Registered session {} for user {}", sessionId, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a session-to-user mapping.
|
|
||||||
* Called when user disconnects.
|
|
||||||
*/
|
|
||||||
public Integer removeSession(String sessionId) {
|
|
||||||
Integer userId = sessionToUser.remove(sessionId);
|
|
||||||
if (userId != null) {
|
|
||||||
log.debug("Removed session {} for user {}", sessionId, userId);
|
|
||||||
}
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a user to a room's connection list.
|
|
||||||
* Called when user subscribes to room topic.
|
|
||||||
*
|
|
||||||
* @param userId The user ID
|
|
||||||
* @param roomNumber The room number
|
|
||||||
* @param sessionId The WebSocket session ID
|
|
||||||
*/
|
|
||||||
public void addUserToRoom(Integer userId, Integer roomNumber, String sessionId) {
|
|
||||||
if (userId == null || roomNumber == null || sessionId == null) {
|
|
||||||
log.warn("Attempted to add user to room with null parameters: userId={}, roomNumber={}, sessionId={}",
|
|
||||||
userId, roomNumber, sessionId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create the map of users for this room
|
|
||||||
Map<Integer, Set<String>> roomUsers = roomConnections.computeIfAbsent(roomNumber, k -> new ConcurrentHashMap<>());
|
|
||||||
|
|
||||||
// Get or create the set of sessions for this user in this room
|
|
||||||
Set<String> userSessions = roomUsers.computeIfAbsent(userId, k -> ConcurrentHashMap.newKeySet());
|
|
||||||
|
|
||||||
// Add the session
|
|
||||||
boolean isNewUser = userSessions.isEmpty();
|
|
||||||
userSessions.add(sessionId);
|
|
||||||
|
|
||||||
int connectedCount = getConnectedUsersCount(roomNumber);
|
|
||||||
|
|
||||||
if (isNewUser) {
|
|
||||||
log.debug("User {} connected to room {} (session: {}). Total connected users: {}",
|
|
||||||
userId, roomNumber, sessionId, connectedCount);
|
|
||||||
} else {
|
|
||||||
log.debug("User {} added additional session to room {} (session: {}). Total sessions for user: {}, Total connected users: {}",
|
|
||||||
userId, roomNumber, sessionId, userSessions.size(), connectedCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify callback to broadcast updated state (only if this is a new user, not just a new session)
|
|
||||||
if (connectionChangeCallback != null && isNewUser) {
|
|
||||||
connectionChangeCallback.accept(roomNumber, connectedCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy method for backward compatibility. Uses sessionId from sessionToUser mapping.
|
|
||||||
* @deprecated Use addUserToRoom(userId, roomNumber, sessionId) instead
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void addUserToRoom(Integer userId, Integer roomNumber) {
|
|
||||||
// Try to find a session for this user (not ideal, but for backward compatibility)
|
|
||||||
String sessionId = sessionToUser.entrySet().stream()
|
|
||||||
.filter(entry -> entry.getValue().equals(userId))
|
|
||||||
.map(Map.Entry::getKey)
|
|
||||||
.findFirst()
|
|
||||||
.orElse("legacy-" + userId + "-" + System.currentTimeMillis());
|
|
||||||
|
|
||||||
addUserToRoom(userId, roomNumber, sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a user's session from a room's connection list.
|
|
||||||
* Only removes the user from the room if this is their last session.
|
|
||||||
* Called when user disconnects or unsubscribes.
|
|
||||||
*
|
|
||||||
* @param userId The user ID
|
|
||||||
* @param roomNumber The room number
|
|
||||||
* @param sessionId The WebSocket session ID
|
|
||||||
*/
|
|
||||||
public void removeUserFromRoom(Integer userId, Integer roomNumber, String sessionId) {
|
|
||||||
if (userId == null || roomNumber == null || sessionId == null) {
|
|
||||||
log.warn("Attempted to remove user from room with null parameters: userId={}, roomNumber={}, sessionId={}",
|
|
||||||
userId, roomNumber, sessionId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<Integer, Set<String>> roomUsers = roomConnections.get(roomNumber);
|
|
||||||
if (roomUsers == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> userSessions = roomUsers.get(userId);
|
|
||||||
if (userSessions == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the session
|
|
||||||
boolean removed = userSessions.remove(sessionId);
|
|
||||||
if (!removed) {
|
|
||||||
log.debug("Session {} not found for user {} in room {}", sessionId, userId, roomNumber);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this was the last session for this user in this room
|
|
||||||
boolean wasLastSession = userSessions.isEmpty();
|
|
||||||
|
|
||||||
if (wasLastSession) {
|
|
||||||
// Remove the user from the room
|
|
||||||
roomUsers.remove(userId);
|
|
||||||
log.debug("User {} disconnected from room {} (last session: {}). Total connected users: {}",
|
|
||||||
userId, roomNumber, sessionId, getConnectedUsersCount(roomNumber));
|
|
||||||
} else {
|
|
||||||
log.debug("User {} removed session from room {} (session: {}). Remaining sessions: {}, Total connected users: {}",
|
|
||||||
userId, roomNumber, sessionId, userSessions.size(), getConnectedUsersCount(roomNumber));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up empty room
|
|
||||||
if (roomUsers.isEmpty()) {
|
|
||||||
roomConnections.remove(roomNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
int connectedCount = getConnectedUsersCount(roomNumber);
|
|
||||||
|
|
||||||
// Notify callback to broadcast updated state (only if user was actually removed)
|
|
||||||
if (connectionChangeCallback != null && wasLastSession) {
|
|
||||||
connectionChangeCallback.accept(roomNumber, connectedCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy method for backward compatibility. Removes all sessions for the user.
|
|
||||||
* @deprecated Use removeUserFromRoom(userId, roomNumber, sessionId) instead
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void removeUserFromRoom(Integer userId, Integer roomNumber) {
|
|
||||||
// Remove all sessions for this user in this room
|
|
||||||
Map<Integer, Set<String>> roomUsers = roomConnections.get(roomNumber);
|
|
||||||
if (roomUsers == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> userSessions = roomUsers.get(userId);
|
|
||||||
if (userSessions == null || userSessions.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all sessions (create a copy to avoid concurrent modification)
|
|
||||||
Set<String> sessionsToRemove = new java.util.HashSet<>(userSessions);
|
|
||||||
for (String sessionId : sessionsToRemove) {
|
|
||||||
removeUserFromRoom(userId, roomNumber, sessionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a specific session from all rooms.
|
|
||||||
* Only removes the user from a room if this is their last session in that room.
|
|
||||||
* Called when a session disconnects completely.
|
|
||||||
*
|
|
||||||
* @param userId The user ID
|
|
||||||
* @param sessionId The WebSocket session ID
|
|
||||||
*/
|
|
||||||
public void removeUserFromAllRooms(Integer userId, String sessionId) {
|
|
||||||
if (userId == null || sessionId == null) {
|
|
||||||
log.warn("Attempted to remove user from all rooms with null parameters: userId={}, sessionId={}",
|
|
||||||
userId, sessionId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate through all rooms and remove this session
|
|
||||||
roomConnections.forEach((roomNumber, roomUsers) -> {
|
|
||||||
Set<String> userSessions = roomUsers.get(userId);
|
|
||||||
if (userSessions != null && userSessions.contains(sessionId)) {
|
|
||||||
// Use the existing method which handles the logic correctly
|
|
||||||
removeUserFromRoom(userId, roomNumber, sessionId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy method that removes all sessions for a user from all rooms.
|
|
||||||
* @deprecated Use removeUserFromAllRooms(userId, sessionId) instead
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void removeUserFromAllRooms(Integer userId) {
|
|
||||||
if (userId == null) {
|
|
||||||
log.warn("Attempted to remove null user from all rooms");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all sessions for this user and remove them
|
|
||||||
roomConnections.forEach((roomNumber, roomUsers) -> {
|
|
||||||
Set<String> userSessions = roomUsers.get(userId);
|
|
||||||
if (userSessions != null && !userSessions.isEmpty()) {
|
|
||||||
// Remove all sessions (create a copy to avoid concurrent modification)
|
|
||||||
Set<String> sessionsToRemove = new java.util.HashSet<>(userSessions);
|
|
||||||
for (String sessionId : sessionsToRemove) {
|
|
||||||
removeUserFromRoom(userId, roomNumber, sessionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a user from all rooms by session ID.
|
|
||||||
* Used when principal is lost during disconnect.
|
|
||||||
*
|
|
||||||
* @param sessionId The WebSocket session ID
|
|
||||||
*/
|
|
||||||
public void removeUserFromAllRoomsBySession(String sessionId) {
|
|
||||||
if (sessionId == null) {
|
|
||||||
log.warn("Attempted to remove user from all rooms with null sessionId");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Integer userId = sessionToUser.get(sessionId);
|
|
||||||
if (userId != null) {
|
|
||||||
// Remove this specific session from all rooms
|
|
||||||
removeUserFromAllRooms(userId, sessionId);
|
|
||||||
// Also remove session mapping
|
|
||||||
removeSession(sessionId);
|
|
||||||
} else {
|
|
||||||
log.warn("Session {} not found in session-to-user mapping", sessionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the count of connected users in a room.
|
|
||||||
* Counts unique users, not sessions (a user with multiple sessions counts as 1).
|
|
||||||
*/
|
|
||||||
public int getConnectedUsersCount(Integer roomNumber) {
|
|
||||||
Map<Integer, Set<String>> roomUsers = roomConnections.get(roomNumber);
|
|
||||||
return roomUsers != null ? roomUsers.size() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a user is connected to a room.
|
|
||||||
* Returns true if the user has at least one active session in the room.
|
|
||||||
*/
|
|
||||||
public boolean isUserConnectedToRoom(Integer userId, Integer roomNumber) {
|
|
||||||
Map<Integer, Set<String>> roomUsers = roomConnections.get(roomNumber);
|
|
||||||
if (roomUsers == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Set<String> userSessions = roomUsers.get(userId);
|
|
||||||
return userSessions != null && !userSessions.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the list of user IDs currently connected (viewing) a room.
|
|
||||||
* Used by admin room management.
|
|
||||||
*/
|
|
||||||
public List<Integer> getConnectedUserIds(Integer roomNumber) {
|
|
||||||
Map<Integer, Set<String>> roomUsers = roomConnections.get(roomNumber);
|
|
||||||
if (roomUsers == null || roomUsers.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
return roomUsers.keySet().stream().sorted().collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -9,13 +9,11 @@ import com.lottery.lottery.model.UserA;
|
|||||||
import com.lottery.lottery.model.UserB;
|
import com.lottery.lottery.model.UserB;
|
||||||
import com.lottery.lottery.model.UserD;
|
import com.lottery.lottery.model.UserD;
|
||||||
import com.lottery.lottery.model.UserTaskClaim;
|
import com.lottery.lottery.model.UserTaskClaim;
|
||||||
import com.lottery.lottery.model.UserDailyBonusClaim;
|
|
||||||
import com.lottery.lottery.repository.TaskRepository;
|
import com.lottery.lottery.repository.TaskRepository;
|
||||||
import com.lottery.lottery.repository.UserARepository;
|
import com.lottery.lottery.repository.UserARepository;
|
||||||
import com.lottery.lottery.repository.UserBRepository;
|
import com.lottery.lottery.repository.UserBRepository;
|
||||||
import com.lottery.lottery.repository.UserDRepository;
|
import com.lottery.lottery.repository.UserDRepository;
|
||||||
import com.lottery.lottery.repository.UserTaskClaimRepository;
|
import com.lottery.lottery.repository.UserTaskClaimRepository;
|
||||||
import com.lottery.lottery.repository.UserDailyBonusClaimRepository;
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -35,7 +33,6 @@ public class TaskService {
|
|||||||
|
|
||||||
private final TaskRepository taskRepository;
|
private final TaskRepository taskRepository;
|
||||||
private final UserTaskClaimRepository userTaskClaimRepository;
|
private final UserTaskClaimRepository userTaskClaimRepository;
|
||||||
private final UserDailyBonusClaimRepository userDailyBonusClaimRepository;
|
|
||||||
private final UserDRepository userDRepository;
|
private final UserDRepository userDRepository;
|
||||||
private final UserBRepository userBRepository;
|
private final UserBRepository userBRepository;
|
||||||
private final UserARepository userARepository;
|
private final UserARepository userARepository;
|
||||||
@@ -76,6 +73,7 @@ public class TaskService {
|
|||||||
final List<Integer> finalClaimedTaskIds = claimedTaskIds;
|
final List<Integer> finalClaimedTaskIds = claimedTaskIds;
|
||||||
|
|
||||||
return tasks.stream()
|
return tasks.stream()
|
||||||
|
.filter(task -> !"daily".equals(task.getType()))
|
||||||
.filter(task -> !finalClaimedTaskIds.contains(task.getId()))
|
.filter(task -> !finalClaimedTaskIds.contains(task.getId()))
|
||||||
.filter(task -> isReferralTaskEnabled(task))
|
.filter(task -> isReferralTaskEnabled(task))
|
||||||
.map(task -> {
|
.map(task -> {
|
||||||
@@ -219,15 +217,19 @@ public class TaskService {
|
|||||||
|
|
||||||
Task task = taskOpt.get();
|
Task task = taskOpt.get();
|
||||||
|
|
||||||
|
// Daily bonus removed - reject daily task claims
|
||||||
|
if ("daily".equals(task.getType())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Reject claim if this referral task (50 or 100 friends) is temporarily disabled
|
// Reject claim if this referral task (50 or 100 friends) is temporarily disabled
|
||||||
if (!isReferralTaskEnabled(task)) {
|
if (!isReferralTaskEnabled(task)) {
|
||||||
log.debug("Task disabled by feature switch: taskId={}, type={}, requirement={}", taskId, task.getType(), task.getRequirement());
|
log.debug("Task disabled by feature switch: taskId={}, type={}, requirement={}", taskId, task.getType(), task.getRequirement());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-daily tasks, check if already claimed FIRST to prevent abuse
|
// Check if already claimed to prevent abuse
|
||||||
// This prevents users from claiming rewards multiple times by leaving/rejoining channels
|
if (userTaskClaimRepository.existsByUserIdAndTaskId(userId, taskId)) {
|
||||||
if (!"daily".equals(task.getType()) && userTaskClaimRepository.existsByUserIdAndTaskId(userId, taskId)) {
|
|
||||||
log.debug("Task already claimed: userId={}, taskId={}", userId, taskId);
|
log.debug("Task already claimed: userId={}, taskId={}", userId, taskId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -239,44 +241,18 @@ public class TaskService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For daily tasks, save to user_daily_bonus_claims table with user info
|
// Save to user_task_claims table
|
||||||
if ("daily".equals(task.getType())) {
|
UserTaskClaim claim = UserTaskClaim.builder()
|
||||||
// Get user data for the claim record
|
.userId(userId)
|
||||||
Optional<UserA> userOpt = userARepository.findById(userId);
|
.taskId(taskId)
|
||||||
String avatarUrl = null;
|
.build();
|
||||||
String screenName = "-";
|
userTaskClaimRepository.save(claim);
|
||||||
if (userOpt.isPresent()) {
|
|
||||||
UserA user = userOpt.get();
|
|
||||||
avatarUrl = user.getAvatarUrl();
|
|
||||||
screenName = user.getScreenName() != null ? user.getScreenName() : "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to user_daily_bonus_claims table
|
|
||||||
UserDailyBonusClaim dailyClaim = UserDailyBonusClaim.builder()
|
|
||||||
.userId(userId)
|
|
||||||
.avatarUrl(avatarUrl)
|
|
||||||
.screenName(screenName)
|
|
||||||
.build();
|
|
||||||
userDailyBonusClaimRepository.save(dailyClaim);
|
|
||||||
} else {
|
|
||||||
// For non-daily tasks, save to user_task_claims table
|
|
||||||
UserTaskClaim claim = UserTaskClaim.builder()
|
|
||||||
.userId(userId)
|
|
||||||
.taskId(taskId)
|
|
||||||
.build();
|
|
||||||
userTaskClaimRepository.save(claim);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give reward (rewardAmount is already in bigint format)
|
// Give reward (rewardAmount is already in bigint format)
|
||||||
giveReward(userId, task.getRewardAmount());
|
giveReward(userId, task.getRewardAmount());
|
||||||
|
|
||||||
// Create transaction - use DAILY_BONUS for daily tasks, TASK_BONUS for others
|
|
||||||
try {
|
try {
|
||||||
if ("daily".equals(task.getType())) {
|
transactionService.createTaskBonusTransaction(userId, task.getRewardAmount(), taskId);
|
||||||
transactionService.createDailyBonusTransaction(userId, task.getRewardAmount());
|
|
||||||
} else {
|
|
||||||
transactionService.createTaskBonusTransaction(userId, task.getRewardAmount(), taskId);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error creating transaction: userId={}, taskId={}, type={}", userId, taskId, task.getType(), e);
|
log.error("Error creating transaction: userId={}, taskId={}, type={}", userId, taskId, task.getType(), e);
|
||||||
// Continue even if transaction record creation fails
|
// Continue even if transaction record creation fails
|
||||||
@@ -343,21 +319,8 @@ public class TaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("daily".equals(task.getType())) {
|
if ("daily".equals(task.getType())) {
|
||||||
// For daily bonus, check if 24 hours have passed since last claim
|
// Daily bonus removed - never completed
|
||||||
// Use user_daily_bonus_claims table instead of user_task_claims
|
return false;
|
||||||
Optional<UserDailyBonusClaim> claimOpt = userDailyBonusClaimRepository.findFirstByUserIdOrderByClaimedAtDesc(userId);
|
|
||||||
if (claimOpt.isEmpty()) {
|
|
||||||
// Never claimed, so it's available
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserDailyBonusClaim claim = claimOpt.get();
|
|
||||||
LocalDateTime claimedAt = claim.getClaimedAt();
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
|
||||||
long hoursSinceClaim = java.time.Duration.between(claimedAt, now).toHours();
|
|
||||||
|
|
||||||
// Available if 24 hours or more have passed
|
|
||||||
return hoursSinceClaim >= 24;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -367,58 +330,14 @@ public class TaskService {
|
|||||||
* Gets daily bonus status for a user.
|
* Gets daily bonus status for a user.
|
||||||
* Returns availability status and cooldown time if on cooldown.
|
* Returns availability status and cooldown time if on cooldown.
|
||||||
*/
|
*/
|
||||||
|
/** Daily bonus removed - always returns unavailable. */
|
||||||
public com.lottery.lottery.dto.DailyBonusStatusDto getDailyBonusStatus(Integer userId) {
|
public com.lottery.lottery.dto.DailyBonusStatusDto getDailyBonusStatus(Integer userId) {
|
||||||
// Find daily bonus task
|
return com.lottery.lottery.dto.DailyBonusStatusDto.builder()
|
||||||
List<Task> dailyTasks = taskRepository.findByTypeOrderByDisplayOrderAsc("daily");
|
.taskId(null)
|
||||||
if (dailyTasks.isEmpty()) {
|
.available(false)
|
||||||
log.warn("Daily bonus task not found");
|
.cooldownSeconds(null)
|
||||||
return com.lottery.lottery.dto.DailyBonusStatusDto.builder()
|
.rewardAmount(0L)
|
||||||
.available(false)
|
.build();
|
||||||
.cooldownSeconds(0L)
|
|
||||||
.rewardAmount(0L)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
Task dailyTask = dailyTasks.get(0);
|
|
||||||
|
|
||||||
// Check if user has claimed before using user_daily_bonus_claims table
|
|
||||||
Optional<UserDailyBonusClaim> claimOpt = userDailyBonusClaimRepository.findFirstByUserIdOrderByClaimedAtDesc(userId);
|
|
||||||
|
|
||||||
if (claimOpt.isEmpty()) {
|
|
||||||
// Never claimed, so it's available
|
|
||||||
return com.lottery.lottery.dto.DailyBonusStatusDto.builder()
|
|
||||||
.taskId(dailyTask.getId())
|
|
||||||
.available(true)
|
|
||||||
.cooldownSeconds(null)
|
|
||||||
.rewardAmount(dailyTask.getRewardAmount())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cooldown
|
|
||||||
UserDailyBonusClaim claim = claimOpt.get();
|
|
||||||
LocalDateTime claimedAt = claim.getClaimedAt();
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
|
||||||
long secondsSinceClaim = java.time.Duration.between(claimedAt, now).getSeconds();
|
|
||||||
long hoursSinceClaim = secondsSinceClaim / 3600;
|
|
||||||
|
|
||||||
if (hoursSinceClaim >= 24) {
|
|
||||||
// Cooldown expired, available
|
|
||||||
return com.lottery.lottery.dto.DailyBonusStatusDto.builder()
|
|
||||||
.taskId(dailyTask.getId())
|
|
||||||
.available(true)
|
|
||||||
.cooldownSeconds(null)
|
|
||||||
.rewardAmount(dailyTask.getRewardAmount())
|
|
||||||
.build();
|
|
||||||
} else {
|
|
||||||
// Still on cooldown
|
|
||||||
long secondsUntilAvailable = (24 * 3600) - secondsSinceClaim;
|
|
||||||
return com.lottery.lottery.dto.DailyBonusStatusDto.builder()
|
|
||||||
.taskId(dailyTask.getId())
|
|
||||||
.available(false)
|
|
||||||
.cooldownSeconds(secondsUntilAvailable)
|
|
||||||
.rewardAmount(dailyTask.getRewardAmount())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -430,47 +349,9 @@ public class TaskService {
|
|||||||
* @param languageCode User's language code for localization (e.g., "EN", "RU")
|
* @param languageCode User's language code for localization (e.g., "EN", "RU")
|
||||||
* @return List of RecentBonusClaimDto with avatar URL, screen name, and formatted claim timestamp
|
* @return List of RecentBonusClaimDto with avatar URL, screen name, and formatted claim timestamp
|
||||||
*/
|
*/
|
||||||
|
/** Daily bonus removed - always returns empty list. */
|
||||||
public List<RecentBonusClaimDto> getRecentDailyBonusClaims(String timezone, String languageCode) {
|
public List<RecentBonusClaimDto> getRecentDailyBonusClaims(String timezone, String languageCode) {
|
||||||
// Get recent claims - simple query, no JOINs needed
|
return List.of();
|
||||||
List<UserDailyBonusClaim> claims = userDailyBonusClaimRepository.findTop50ByOrderByClaimedAtDesc();
|
|
||||||
|
|
||||||
// Determine timezone to use
|
|
||||||
java.time.ZoneId zoneId;
|
|
||||||
try {
|
|
||||||
zoneId = (timezone != null && !timezone.trim().isEmpty())
|
|
||||||
? java.time.ZoneId.of(timezone)
|
|
||||||
: java.time.ZoneId.of("UTC");
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Invalid timezone, fallback to UTC
|
|
||||||
zoneId = java.time.ZoneId.of("UTC");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get localized "at" word
|
|
||||||
String atWord = localizationService.getMessage("dateTime.at", languageCode);
|
|
||||||
if (atWord == null || atWord.isEmpty()) {
|
|
||||||
atWord = "at"; // Fallback to English
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create formatter with localized "at" word
|
|
||||||
final java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("dd.MM '" + atWord + "' HH:mm")
|
|
||||||
.withZone(zoneId);
|
|
||||||
|
|
||||||
// Convert to DTOs with formatted date
|
|
||||||
return claims.stream()
|
|
||||||
.map(claim -> {
|
|
||||||
// Convert LocalDateTime to Instant (assuming it's stored in UTC)
|
|
||||||
// LocalDateTime doesn't have timezone info, so we treat it as UTC
|
|
||||||
java.time.Instant instant = claim.getClaimedAt().atZone(java.time.ZoneId.of("UTC")).toInstant();
|
|
||||||
String formattedDate = formatter.format(instant);
|
|
||||||
|
|
||||||
return RecentBonusClaimDto.builder()
|
|
||||||
.avatarUrl(claim.getAvatarUrl())
|
|
||||||
.screenName(claim.getScreenName())
|
|
||||||
.claimedAt(claim.getClaimedAt())
|
|
||||||
.date(formattedDate)
|
|
||||||
.build();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -63,44 +63,6 @@ public class TransactionService {
|
|||||||
log.debug("Created withdrawal transaction: userId={}, amount={}", userId, amount);
|
log.debug("Created withdrawal transaction: userId={}, amount={}", userId, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a win transaction.
|
|
||||||
*
|
|
||||||
* @param userId User ID
|
|
||||||
* @param amount Amount in bigint format (positive, total payout)
|
|
||||||
* @param roundId Round ID
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public void createWinTransaction(Integer userId, Long amount, Long roundId) {
|
|
||||||
Transaction transaction = Transaction.builder()
|
|
||||||
.userId(userId)
|
|
||||||
.amount(amount)
|
|
||||||
.type(Transaction.TransactionType.WIN)
|
|
||||||
.roundId(roundId)
|
|
||||||
.build();
|
|
||||||
transactionRepository.save(transaction);
|
|
||||||
log.debug("Created win transaction: userId={}, amount={}, roundId={}", userId, amount, roundId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a bet transaction.
|
|
||||||
*
|
|
||||||
* @param userId User ID
|
|
||||||
* @param amount Amount in bigint format (positive, will be stored as negative)
|
|
||||||
* @param roundId Round ID
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public void createBetTransaction(Integer userId, Long amount, Long roundId) {
|
|
||||||
Transaction transaction = Transaction.builder()
|
|
||||||
.userId(userId)
|
|
||||||
.amount(-amount) // Store as negative
|
|
||||||
.type(Transaction.TransactionType.BET)
|
|
||||||
.roundId(roundId)
|
|
||||||
.build();
|
|
||||||
transactionRepository.save(transaction);
|
|
||||||
log.debug("Created bet transaction: userId={}, amount={}, roundId={}", userId, amount, roundId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a task bonus transaction.
|
* Creates a task bonus transaction.
|
||||||
*
|
*
|
||||||
@@ -120,24 +82,6 @@ public class TransactionService {
|
|||||||
log.debug("Created task bonus transaction: userId={}, amount={}, taskId={}", userId, amount, taskId);
|
log.debug("Created task bonus transaction: userId={}, amount={}, taskId={}", userId, amount, taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a daily bonus transaction.
|
|
||||||
*
|
|
||||||
* @param userId User ID
|
|
||||||
* @param amount Amount in bigint format (positive)
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public void createDailyBonusTransaction(Integer userId, Long amount) {
|
|
||||||
Transaction transaction = Transaction.builder()
|
|
||||||
.userId(userId)
|
|
||||||
.amount(amount)
|
|
||||||
.type(Transaction.TransactionType.DAILY_BONUS)
|
|
||||||
.taskId(null) // Daily bonus doesn't have taskId
|
|
||||||
.build();
|
|
||||||
transactionRepository.save(transaction);
|
|
||||||
log.debug("Created daily bonus transaction: userId={}, amount={}", userId, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a cancellation of withdrawal transaction.
|
* Creates a cancellation of withdrawal transaction.
|
||||||
* Used when admin cancels a payout - refunds tickets to user.
|
* Used when admin cancels a payout - refunds tickets to user.
|
||||||
@@ -153,7 +97,6 @@ public class TransactionService {
|
|||||||
.amount(amount) // Positive amount (credit back to user)
|
.amount(amount) // Positive amount (credit back to user)
|
||||||
.type(Transaction.TransactionType.CANCELLATION_OF_WITHDRAWAL)
|
.type(Transaction.TransactionType.CANCELLATION_OF_WITHDRAWAL)
|
||||||
.taskId(null)
|
.taskId(null)
|
||||||
.roundId(null)
|
|
||||||
.createdAt(createdAt) // Set custom timestamp (@PrePersist will check if null)
|
.createdAt(createdAt) // Set custom timestamp (@PrePersist will check if null)
|
||||||
.build();
|
.build();
|
||||||
transactionRepository.save(transaction);
|
transactionRepository.save(transaction);
|
||||||
@@ -203,27 +146,19 @@ public class TransactionService {
|
|||||||
// Format date
|
// Format date
|
||||||
String date = formatter.format(transaction.getCreatedAt());
|
String date = formatter.format(transaction.getCreatedAt());
|
||||||
|
|
||||||
// Send enum value as string (e.g., "TASK_BONUS", "WIN") - frontend will handle localization
|
|
||||||
String typeEnumValue = transaction.getType().name();
|
String typeEnumValue = transaction.getType().name();
|
||||||
|
Integer taskIdToInclude = transaction.getTaskId();
|
||||||
// For DAILY_BONUS, don't include taskId (it should be null)
|
|
||||||
// For TASK_BONUS, include taskId
|
|
||||||
Integer taskIdToInclude = (transaction.getType() == Transaction.TransactionType.DAILY_BONUS)
|
|
||||||
? null
|
|
||||||
: transaction.getTaskId();
|
|
||||||
|
|
||||||
return TransactionDto.builder()
|
return TransactionDto.builder()
|
||||||
.amount(transaction.getAmount())
|
.amount(transaction.getAmount())
|
||||||
.date(date)
|
.date(date)
|
||||||
.type(typeEnumValue) // Send enum value, not localized string
|
.type(typeEnumValue)
|
||||||
.taskId(taskIdToInclude)
|
.taskId(taskIdToInclude)
|
||||||
.roundId(transaction.getRoundId())
|
|
||||||
.build();
|
.build();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Transaction type localization is now handled in the frontend.
|
// Transaction type localization is handled in the frontend.
|
||||||
// Backend sends enum values (TASK_BONUS, WIN, etc.) and frontend translates them.
|
|
||||||
// This method is no longer used but kept for reference.
|
// This method is no longer used but kept for reference.
|
||||||
@Deprecated
|
@Deprecated
|
||||||
private String mapTransactionTypeToDisplayName(Transaction.TransactionType type, String languageCode) {
|
private String mapTransactionTypeToDisplayName(Transaction.TransactionType type, String languageCode) {
|
||||||
|
|||||||
@@ -95,16 +95,6 @@ app:
|
|||||||
# Avatar URL cache TTL in minutes (default: 5 minutes)
|
# Avatar URL cache TTL in minutes (default: 5 minutes)
|
||||||
cache-ttl-minutes: ${APP_AVATAR_CACHE_TTL_MINUTES:5}
|
cache-ttl-minutes: ${APP_AVATAR_CACHE_TTL_MINUTES:5}
|
||||||
|
|
||||||
websocket:
|
|
||||||
# Allowed origins for WebSocket CORS (comma-separated)
|
|
||||||
# Default includes production domain and Telegram WebView domains
|
|
||||||
allowed-origins: ${APP_WEBSOCKET_ALLOWED_ORIGINS:https://win-spin.live,https://lottery-fe-production.up.railway.app,https://web.telegram.org,https://webk.telegram.org,https://webz.telegram.org}
|
|
||||||
|
|
||||||
# Lottery bot scheduler: auto-joins bots from lottery_bot_configs into joinable rounds. Toggle via admin Feature Switches (lottery_bot_scheduler_enabled).
|
|
||||||
# Bet amount is decided in-process by persona + loss-streak and zone logic (no external API).
|
|
||||||
lottery-bot:
|
|
||||||
schedule-fixed-delay-ms: ${APP_LOTTERY_BOT_SCHEDULE_FIXED_DELAY_MS:5000}
|
|
||||||
|
|
||||||
# Secret token for remote bet API (GET /api/remotebet/{token}?user_id=&room=&amount=). No auth; enable via Feature Switchers in admin.
|
# Secret token for remote bet API (GET /api/remotebet/{token}?user_id=&room=&amount=). No auth; enable via Feature Switchers in admin.
|
||||||
remote-bet:
|
remote-bet:
|
||||||
token: ${APP_REMOTE_BET_TOKEN:}
|
token: ${APP_REMOTE_BET_TOKEN:}
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ CREATE TABLE transactions (
|
|||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INT NOT NULL,
|
user_id INT NOT NULL,
|
||||||
amount BIGINT NOT NULL COMMENT 'Amount in bigint format (positive for credits, negative for debits)',
|
amount BIGINT NOT NULL COMMENT 'Amount in bigint format (positive for credits, negative for debits)',
|
||||||
type VARCHAR(50) NOT NULL COMMENT 'Transaction type: DEPOSIT, WITHDRAWAL, WIN, LOSS, TASK_BONUS',
|
type VARCHAR(50) NOT NULL COMMENT 'Transaction type: DEPOSIT, WITHDRAWAL, TASK_BONUS, CANCELLATION_OF_WITHDRAWAL',
|
||||||
task_id INT NULL COMMENT 'Task ID for TASK_BONUS type',
|
task_id INT NULL COMMENT 'Task ID for TASK_BONUS type',
|
||||||
round_id BIGINT NULL COMMENT 'Round ID for WIN/LOSS type',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
INDEX idx_user_id_created_at (user_id, created_at DESC),
|
INDEX idx_user_id_created_at (user_id, created_at DESC),
|
||||||
INDEX idx_user_id_type (user_id, type),
|
INDEX idx_user_id_type (user_id, type),
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
-- Add index on game_rounds for join query optimization
|
-- Add index on transactions for cleanup by created_at
|
||||||
-- This helps with the query: SELECT p FROM GameRoundParticipant p WHERE p.userId = :userId
|
|
||||||
-- AND p.round.phase = 'RESOLUTION' AND p.round.resolvedAt IS NOT NULL ORDER BY p.round.resolvedAt DESC
|
|
||||||
CREATE INDEX idx_round_phase_resolved ON game_rounds (id, phase, resolved_at DESC);
|
|
||||||
|
|
||||||
-- Add index on game_round_participants for cleanup by joined_at
|
|
||||||
CREATE INDEX idx_joined_at ON game_round_participants (joined_at);
|
|
||||||
|
|
||||||
-- Add index on transactions for game history queries (filtering by WIN type) and cleanup by created_at
|
|
||||||
CREATE INDEX idx_type_created_at ON transactions (type, created_at);
|
CREATE INDEX idx_type_created_at ON transactions (type, created_at);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
-- Remove unused index on game_round_participants
|
|
||||||
-- This index was only used for cleanup, which is no longer performed on this table
|
|
||||||
-- Participants are now deleted immediately after rounds finish
|
|
||||||
DROP INDEX idx_joined_at ON game_round_participants;
|
|
||||||
|
|
||||||
-- Update comment: idx_type_created_at is now used for game history queries (filtering by WIN type)
|
|
||||||
-- The cleanup query no longer filters by type, but the index is still useful for game history
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
-- Insert Daily Bonus task
|
-- Daily bonus task removed (user_daily_bonus_claims table and related logic removed).
|
||||||
-- reward_amount is in bigint format (1 ticket = 1000000)
|
|
||||||
-- requirement is 24 hours in milliseconds (86400000), but we'll use 0 as placeholder since we check claimed_at timestamp
|
|
||||||
-- The actual 24h check is done in TaskService.isTaskCompleted() for "daily" type
|
|
||||||
INSERT INTO `tasks` (`type`, `requirement`, `reward_amount`, `reward_type`, `display_order`, `title`, `description`) VALUES
|
|
||||||
('daily', 0, 1000000, 'Tickets', 1, 'Daily Bonus', 'Claim 1 free ticket every 24 hours');
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
-- Create user_daily_bonus_claims table for daily bonus claims
|
|
||||||
-- This table stores daily bonus claims with user information to avoid JOINs
|
|
||||||
CREATE TABLE IF NOT EXISTS `user_daily_bonus_claims` (
|
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
|
||||||
`user_id` int NOT NULL,
|
|
||||||
`avatar_url` varchar(255) DEFAULT NULL,
|
|
||||||
`screen_name` varchar(75) NOT NULL DEFAULT '-',
|
|
||||||
`claimed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_user_id` (`user_id`),
|
|
||||||
KEY `idx_claimed_at` (`claimed_at` DESC),
|
|
||||||
CONSTRAINT `fk_user_daily_bonus_claims_user` FOREIGN KEY (`user_id`) REFERENCES `db_users_a` (`id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- Add rounds_played column to db_users_b table
|
|
||||||
-- This column tracks how many rounds each user has participated in
|
|
||||||
-- Used for third bet bonus logic instead of counting transactions
|
|
||||||
ALTER TABLE `db_users_b`
|
|
||||||
ADD COLUMN `rounds_played` int NOT NULL DEFAULT '0' AFTER `withdraw_count`;
|
|
||||||
|
|
||||||
@@ -36,13 +36,6 @@ CREATE INDEX idx_payouts_status_created_at ON payouts(status, created_at);
|
|||||||
-- Index for payout type filtering
|
-- Index for payout type filtering
|
||||||
CREATE INDEX idx_payouts_type ON payouts(type);
|
CREATE INDEX idx_payouts_type ON payouts(type);
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- game_rounds indexes
|
|
||||||
-- ============================================
|
|
||||||
-- Composite index for queries filtering by phase and resolved_at
|
|
||||||
-- This helps with queries like countByResolvedAtAfter when combined with phase filters
|
|
||||||
CREATE INDEX idx_game_rounds_phase_resolved_at ON game_rounds(phase, resolved_at);
|
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- support_tickets indexes
|
-- support_tickets indexes
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
-- Create game_rooms table
|
|
||||||
CREATE TABLE IF NOT EXISTS game_rooms (
|
|
||||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
room_number INT NOT NULL UNIQUE,
|
|
||||||
current_phase VARCHAR(20) NOT NULL DEFAULT 'WAITING',
|
|
||||||
countdown_end_at TIMESTAMP NULL,
|
|
||||||
total_tickets BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
|
||||||
registered_players INT NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_phase (current_phase),
|
|
||||||
INDEX idx_countdown (countdown_end_at)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Create game_rounds table (completed rounds history)
|
|
||||||
CREATE TABLE IF NOT EXISTS game_rounds (
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
room_id INT NOT NULL,
|
|
||||||
phase VARCHAR(20) NOT NULL,
|
|
||||||
total_tickets BIGINT UNSIGNED NOT NULL,
|
|
||||||
winner_user_id INT NULL,
|
|
||||||
winner_tickets BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
|
||||||
commission BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
|
||||||
payout BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
|
||||||
started_at TIMESTAMP NOT NULL,
|
|
||||||
countdown_started_at TIMESTAMP NULL,
|
|
||||||
countdown_ended_at TIMESTAMP NULL,
|
|
||||||
resolved_at TIMESTAMP NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_room (room_id),
|
|
||||||
INDEX idx_winner (winner_user_id),
|
|
||||||
INDEX idx_resolved (resolved_at),
|
|
||||||
FOREIGN KEY (room_id) REFERENCES game_rooms(id) ON DELETE RESTRICT
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Create game_round_participants table (who joined which round)
|
|
||||||
CREATE TABLE IF NOT EXISTS game_round_participants (
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
round_id BIGINT NOT NULL,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
tickets BIGINT UNSIGNED NOT NULL,
|
|
||||||
joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_round (round_id),
|
|
||||||
INDEX idx_user (user_id),
|
|
||||||
INDEX idx_round_user (round_id, user_id),
|
|
||||||
FOREIGN KEY (round_id) REFERENCES game_rounds(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES db_users_a(id) ON DELETE RESTRICT
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Insert default rooms (Room 1, 2, 3)
|
|
||||||
INSERT INTO game_rooms (room_number, current_phase, total_tickets, registered_players)
|
|
||||||
VALUES (1, 'WAITING', 0, 0),
|
|
||||||
(2, 'WAITING', 0, 0),
|
|
||||||
(3, 'WAITING', 0, 0);
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- Add total_win_after_deposit to db_users_b (bigint: 1 ticket = 1_000_000).
|
|
||||||
-- Reset to 0 on each deposit; incremented by round win amount when user wins; reduced when user creates a payout.
|
|
||||||
ALTER TABLE `db_users_b`
|
|
||||||
ADD COLUMN `total_win_after_deposit` BIGINT NOT NULL DEFAULT 0 AFTER `rounds_played`;
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
-- Runtime feature toggles (e.g. remote bet endpoint). Can be changed from admin panel without restart.
|
-- Runtime feature toggles. Can be changed from admin panel without restart. Kept empty (no seeds).
|
||||||
CREATE TABLE `feature_switches` (
|
CREATE TABLE `feature_switches` (
|
||||||
`key` VARCHAR(64) NOT NULL,
|
`key` VARCHAR(64) NOT NULL,
|
||||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
`enabled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`key`)
|
PRIMARY KEY (`key`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Insert default: remote bet endpoint disabled until explicitly enabled from admin
|
|
||||||
INSERT INTO `feature_switches` (`key`, `enabled`) VALUES ('remote_bet_enabled', 1);
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
-- Rename tickets columns to bet for currency-agnostic naming
|
|
||||||
ALTER TABLE game_rooms CHANGE COLUMN total_tickets total_bet BIGINT UNSIGNED NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE game_rounds CHANGE COLUMN total_tickets total_bet BIGINT UNSIGNED NOT NULL;
|
|
||||||
ALTER TABLE game_rounds CHANGE COLUMN winner_tickets winner_bet BIGINT UNSIGNED NOT NULL DEFAULT 0;
|
|
||||||
ALTER TABLE game_round_participants CHANGE COLUMN tickets bet BIGINT UNSIGNED NOT NULL;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1 @@
|
|||||||
-- Feature switchers for payment (deposits) and payout (withdrawals). Enabled by default.
|
-- Feature switches: no seeds (kept empty).
|
||||||
INSERT INTO `feature_switches` (`key`, `enabled`) VALUES
|
|
||||||
('payment_enabled', 1),
|
|
||||||
('payout_enabled', 1);
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
-- Add composite index for optimized querying of completed rounds with winners
|
|
||||||
-- This index supports the query: WHERE room_id = X AND phase = 'RESOLUTION' AND resolved_at IS NOT NULL AND winner_user_id IS NOT NULL ORDER BY resolved_at DESC
|
|
||||||
-- Index order:
|
|
||||||
-- 1. room_id (exact match, most selective WHERE filter)
|
|
||||||
-- 2. phase (exact match WHERE filter)
|
|
||||||
-- 3. resolved_at (ORDER BY column - placed after equality filters for efficient sorting)
|
|
||||||
-- 4. winner_user_id (IS NOT NULL filter - MySQL can still use index efficiently)
|
|
||||||
-- This allows MySQL to efficiently filter by room_id and phase, then sort by resolved_at using the index
|
|
||||||
CREATE INDEX idx_room_phase_resolved_winner ON game_rounds (room_id, phase, resolved_at, winner_user_id);
|
|
||||||
|
|
||||||
@@ -1,5 +1 @@
|
|||||||
-- Toggle "Invite 50 friends" and "Invite 100 friends" referral tasks. When disabled (0), tasks are hidden and cannot be claimed.
|
-- Feature switches: no seeds (kept empty).
|
||||||
INSERT INTO `feature_switches` (`key`, `enabled`) VALUES
|
|
||||||
('task_referral_50_enabled', 0),
|
|
||||||
('task_referral_100_enabled', 0)
|
|
||||||
ON DUPLICATE KEY UPDATE `key` = VALUES(`key`);
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
-- Safe bot users: when balance < threshold they get 100% win rate at resolution (display unchanged)
|
|
||||||
CREATE TABLE IF NOT EXISTS safe_bot_users (
|
|
||||||
user_id INT NOT NULL PRIMARY KEY,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Flexible bot config: user has fixed win rate (0-1) regardless of bet
|
|
||||||
CREATE TABLE IF NOT EXISTS flexible_bot_configs (
|
|
||||||
user_id INT NOT NULL PRIMARY KEY,
|
|
||||||
win_rate DECIMAL(5,4) NOT NULL COMMENT '0.0000-1.0000',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT chk_win_rate CHECK (win_rate >= 0 AND win_rate <= 1)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
-- Indexes for admin users list sorting (Balance, Profit, Deposits, Withdraws, Rounds, Referrals)
|
-- Indexes for admin users list sorting (Balance, Deposits, Withdraws, Referrals)
|
||||||
-- db_users_b: balance_a, deposit_total, withdraw_total, rounds_played
|
-- db_users_b: balance_a, deposit_total, withdraw_total
|
||||||
CREATE INDEX idx_users_b_balance_a ON db_users_b(balance_a);
|
CREATE INDEX idx_users_b_balance_a ON db_users_b(balance_a);
|
||||||
CREATE INDEX idx_users_b_deposit_total ON db_users_b(deposit_total);
|
CREATE INDEX idx_users_b_deposit_total ON db_users_b(deposit_total);
|
||||||
CREATE INDEX idx_users_b_withdraw_total ON db_users_b(withdraw_total);
|
CREATE INDEX idx_users_b_withdraw_total ON db_users_b(withdraw_total);
|
||||||
CREATE INDEX idx_users_b_rounds_played ON db_users_b(rounds_played);
|
|
||||||
|
|
||||||
-- db_users_d: for referral count (sum of referals_1..5) we filter by referer_id_N; indexes already exist (V34)
|
-- db_users_d: for referral count (sum of referals_1..5) we filter by referer_id_N; indexes already exist (V34)
|
||||||
-- For sorting by total referral count we could use a composite; referer_id_1 is used for "referrals of user X"
|
-- For sorting by total referral count we could use a composite; referer_id_1 is used for "referrals of user X"
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
-- Bot behaviour config: links a real user (db_users_a/b) to play as a bot with time windows, rooms, bet range.
|
|
||||||
-- One config per user; user_id must exist in db_users_a.
|
|
||||||
CREATE TABLE IF NOT EXISTS lottery_bot_configs (
|
|
||||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
room_1 TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Can play room 1',
|
|
||||||
room_2 TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Can play room 2',
|
|
||||||
room_3 TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Can play room 3',
|
|
||||||
time_utc_start TIME NOT NULL COMMENT 'Start of active window (UTC)',
|
|
||||||
time_utc_end TIME NOT NULL COMMENT 'End of active window (UTC)',
|
|
||||||
bet_min BIGINT NOT NULL COMMENT 'Min bet in bigint (1 ticket = 1000000)',
|
|
||||||
bet_max BIGINT NOT NULL COMMENT 'Max bet in bigint',
|
|
||||||
persona VARCHAR(20) NOT NULL DEFAULT 'balanced' COMMENT 'conservative, aggressive, balanced',
|
|
||||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_lottery_bot_configs_user_id (user_id),
|
|
||||||
KEY idx_lottery_bot_configs_active_rooms (active, room_1, room_2, room_3),
|
|
||||||
CONSTRAINT fk_lottery_bot_configs_user FOREIGN KEY (user_id) REFERENCES db_users_a(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- Toggle for lottery bot scheduler (auto-join bots from lottery_bot_configs into joinable rounds). When disabled (0), scheduler skips registration.
|
|
||||||
INSERT INTO `feature_switches` (`key`, `enabled`) VALUES
|
|
||||||
('lottery_bot_scheduler_enabled', 1)
|
|
||||||
ON DUPLICATE KEY UPDATE `key` = VALUES(`key`);
|
|
||||||
@@ -1,20 +1 @@
|
|||||||
-- First NET_WIN promotion: 26.02.2026 12:00 UTC -> 01.03.2026 12:00 UTC
|
-- Promotions: no seeds (tables created in V56).
|
||||||
INSERT INTO promotions (type, start_time, end_time, status) VALUES
|
|
||||||
('NET_WIN', '2026-02-26 12:00:00', '2026-03-01 12:00:00', 'PLANNED');
|
|
||||||
|
|
||||||
-- Rewards: 1 ticket = 1,000,000 in bigint
|
|
||||||
-- place 1: 50,000 tickets = 50000000000
|
|
||||||
-- place 2: 30,000 = 30000000000, 3: 20,000 = 20000000000, 4: 15,000 = 15000000000, 5: 10,000 = 10000000000
|
|
||||||
-- places 6-10: 5,000 each = 5000000000
|
|
||||||
SET @promo_id = LAST_INSERT_ID();
|
|
||||||
INSERT INTO promotions_rewards (promo_id, place, reward) VALUES
|
|
||||||
(@promo_id, 1, 50000000000),
|
|
||||||
(@promo_id, 2, 30000000000),
|
|
||||||
(@promo_id, 3, 20000000000),
|
|
||||||
(@promo_id, 4, 15000000000),
|
|
||||||
(@promo_id, 5, 10000000000),
|
|
||||||
(@promo_id, 6, 5000000000),
|
|
||||||
(@promo_id, 7, 5000000000),
|
|
||||||
(@promo_id, 8, 5000000000),
|
|
||||||
(@promo_id, 9, 5000000000),
|
|
||||||
(@promo_id, 10, 5000000000);
|
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
-- total_reward in tickets (BIGINT: 1 ticket = 1_000_000)
|
-- total_reward in tickets (BIGINT: 1 ticket = 1_000_000)
|
||||||
ALTER TABLE promotions
|
ALTER TABLE promotions
|
||||||
ADD COLUMN total_reward BIGINT NULL DEFAULT NULL COMMENT 'Total prize fund in bigint (1 ticket = 1000000)' AFTER status;
|
ADD COLUMN total_reward BIGINT NULL DEFAULT NULL COMMENT 'Total prize fund in bigint (1 ticket = 1000000)' AFTER status;
|
||||||
|
|
||||||
-- First promo: 150 000 tickets = 150_000_000_000
|
|
||||||
UPDATE promotions SET total_reward = 150000000000 WHERE id = 1;
|
|
||||||
|
|
||||||
-- Index for filtering by status (already have idx_promotions_status)
|
|
||||||
-- total_reward is for display only, no extra index needed
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
-- Update promotion id=1 rewards (place -> tickets; stored as tickets * 1_000_000)
|
|
||||||
-- 1->300k, 2->200k, 3->150k, 4->100k, 5->80k, 6->55k, 7->40k, 8->30k, 9->25k, 10->20k
|
|
||||||
UPDATE promotions_rewards SET reward = 300000000000 WHERE promo_id = 1 AND place = 1;
|
|
||||||
UPDATE promotions_rewards SET reward = 200000000000 WHERE promo_id = 1 AND place = 2;
|
|
||||||
UPDATE promotions_rewards SET reward = 150000000000 WHERE promo_id = 1 AND place = 3;
|
|
||||||
UPDATE promotions_rewards SET reward = 100000000000 WHERE promo_id = 1 AND place = 4;
|
|
||||||
UPDATE promotions_rewards SET reward = 80000000000 WHERE promo_id = 1 AND place = 5;
|
|
||||||
UPDATE promotions_rewards SET reward = 55000000000 WHERE promo_id = 1 AND place = 6;
|
|
||||||
UPDATE promotions_rewards SET reward = 40000000000 WHERE promo_id = 1 AND place = 7;
|
|
||||||
UPDATE promotions_rewards SET reward = 30000000000 WHERE promo_id = 1 AND place = 8;
|
|
||||||
UPDATE promotions_rewards SET reward = 25000000000 WHERE promo_id = 1 AND place = 9;
|
|
||||||
UPDATE promotions_rewards SET reward = 20000000000 WHERE promo_id = 1 AND place = 10;
|
|
||||||
|
|
||||||
-- Total prize fund: 1_000_000 tickets = 1_000_000_000_000
|
|
||||||
UPDATE promotions SET total_reward = 1000000000000 WHERE id = 1;
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
-- Configurations: key-value store for app-wide settings (e.g. lottery bot scheduler).
|
-- Configurations: key-value store for app-wide settings.
|
||||||
CREATE TABLE IF NOT EXISTS configurations (
|
CREATE TABLE IF NOT EXISTS configurations (
|
||||||
`key` VARCHAR(128) NOT NULL PRIMARY KEY,
|
`key` VARCHAR(128) NOT NULL PRIMARY KEY,
|
||||||
value VARCHAR(512) NOT NULL DEFAULT ''
|
value VARCHAR(512) NOT NULL DEFAULT ''
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Bots may join a round only when participant count <= this value (default 1 = join when 0 or 1 participant).
|
|
||||||
INSERT INTO configurations (`key`, value) VALUES ('lottery_bot_max_participants_before_join', '1')
|
|
||||||
ON DUPLICATE KEY UPDATE `key` = VALUES(`key`);
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
-- Index for admin online-users and other flows that fetch "most recent active round" per room.
|
|
||||||
-- Query: WHERE room_id = ? AND phase IN (...) ORDER BY started_at DESC LIMIT 1
|
|
||||||
-- Covers filter by room_id and phase, and sort by started_at without filesort.
|
|
||||||
CREATE INDEX idx_game_rounds_room_phase_started_at ON game_rounds (room_id, phase, started_at DESC);
|
|
||||||
@@ -1,4 +1 @@
|
|||||||
-- When enabled (1), send manual_pay=1 for all crypto payouts. When disabled (0), send manual_pay=1 only for users who completed 50 or 100 referrals (first withdrawal). Default on.
|
-- Feature switches: no seeds (kept empty).
|
||||||
INSERT INTO `feature_switches` (`key`, `enabled`) VALUES
|
|
||||||
('manual_pay_for_all_payouts', 1)
|
|
||||||
ON DUPLICATE KEY UPDATE `key` = VALUES(`key`);
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
-- Per-user withdrawal restriction. When 1, the user cannot create any payout request (STARS, GIFT, CRYPTO).
|
-- Per-user withdrawal restriction. When 1, the user cannot create any payout request (STARS, GIFT, CRYPTO).
|
||||||
ALTER TABLE `db_users_b` ADD COLUMN `withdrawals_disabled` TINYINT(1) NOT NULL DEFAULT 0 AFTER `total_win_after_deposit`;
|
ALTER TABLE `db_users_b` ADD COLUMN `withdrawals_disabled` TINYINT(1) NOT NULL DEFAULT 0 AFTER `withdraw_count`;
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
-- Two new promotions: NET_WIN_MAX_BET and REF_COUNT, start 4 March 2026 14:00 UTC, same rewards as promotion 1 (V60)
|
|
||||||
-- End time: 11 March 2026 14:00 UTC (1 week)
|
|
||||||
INSERT INTO promotions (type, start_time, end_time, status, total_reward) VALUES
|
|
||||||
('NET_WIN_MAX_BET', '2026-03-04 14:00:00', '2026-03-11 14:00:00', 'PLANNED', 1000000000000),
|
|
||||||
('REF_COUNT', '2026-03-04 14:00:00', '2026-03-11 14:00:00', 'PLANNED', 1000000000000);
|
|
||||||
|
|
||||||
-- NET_WIN_MAX_BET rewards (same as promotion 1)
|
|
||||||
SET @promo_max_bet = (SELECT id FROM promotions WHERE type = 'NET_WIN_MAX_BET' AND start_time = '2026-03-04 14:00:00' LIMIT 1);
|
|
||||||
INSERT INTO promotions_rewards (promo_id, place, reward) VALUES
|
|
||||||
(@promo_max_bet, 1, 300000000000),
|
|
||||||
(@promo_max_bet, 2, 200000000000),
|
|
||||||
(@promo_max_bet, 3, 150000000000),
|
|
||||||
(@promo_max_bet, 4, 100000000000),
|
|
||||||
(@promo_max_bet, 5, 80000000000),
|
|
||||||
(@promo_max_bet, 6, 55000000000),
|
|
||||||
(@promo_max_bet, 7, 40000000000),
|
|
||||||
(@promo_max_bet, 8, 30000000000),
|
|
||||||
(@promo_max_bet, 9, 25000000000),
|
|
||||||
(@promo_max_bet, 10, 20000000000);
|
|
||||||
|
|
||||||
-- REF_COUNT rewards (same structure)
|
|
||||||
SET @promo_ref = (SELECT id FROM promotions WHERE type = 'REF_COUNT' AND start_time = '2026-03-04 14:00:00' LIMIT 1);
|
|
||||||
INSERT INTO promotions_rewards (promo_id, place, reward) VALUES
|
|
||||||
(@promo_ref, 1, 300000000000),
|
|
||||||
(@promo_ref, 2, 200000000000),
|
|
||||||
(@promo_ref, 3, 150000000000),
|
|
||||||
(@promo_ref, 4, 100000000000),
|
|
||||||
(@promo_ref, 5, 80000000000),
|
|
||||||
(@promo_ref, 6, 55000000000),
|
|
||||||
(@promo_ref, 7, 40000000000),
|
|
||||||
(@promo_ref, 8, 30000000000),
|
|
||||||
(@promo_ref, 9, 25000000000),
|
|
||||||
(@promo_ref, 10, 20000000000);
|
|
||||||
Reference in New Issue
Block a user