init
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.3.5</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.tailbet</groupId>
|
||||||
|
<artifactId>guess-game-backend</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>guess-game-backend</name>
|
||||||
|
<description>尾数竞猜游戏后端</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<mybatis-plus.version>3.5.9</mybatis-plus.version>
|
||||||
|
<spring-cloud.version>2023.0.3</spring-cloud.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot Web -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Validation -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Redis -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MyBatis Plus -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
<version>${mybatis-plus.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MySQL Driver -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Hutool 工具库 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>5.8.30</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JWT -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Test -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.tailbet;
|
||||||
|
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
|
@MapperScan("com.tailbet.mapper")
|
||||||
|
public class GuessGameApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(GuessGameApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.tailbet.config;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.model.vo.LoginUserVo;
|
||||||
|
import com.tailbet.model.vo.R;
|
||||||
|
import com.tailbet.model.vo.UserContext;
|
||||||
|
import com.tailbet.util.JwtUtil;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录认证拦截器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
// 跳过OPTIONS请求
|
||||||
|
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String uri = request.getRequestURI();
|
||||||
|
// 放行登录注册接口
|
||||||
|
if (uri.contains("/api/user/register") || uri.contains("/api/user/login")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = extractToken(request);
|
||||||
|
if (StrUtil.isBlank(token)) {
|
||||||
|
writeUnauthorizedResponse(response, "未登录");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token是否过期
|
||||||
|
if (jwtUtil.isTokenExpired(token)) {
|
||||||
|
writeUnauthorizedResponse(response, "登录已过期");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查Redis中token是否有效
|
||||||
|
Long userId = jwtUtil.getUserId(token);
|
||||||
|
if (userId == null) {
|
||||||
|
writeUnauthorizedResponse(response, "无效的token");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户
|
||||||
|
User user = userMapper.selectById(userId);
|
||||||
|
if (user == null || user.getStatus() == 0) {
|
||||||
|
writeUnauthorizedResponse(response, "用户不存在或已被封禁");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置用户上下文
|
||||||
|
UserContext.setUser(LoginUserVo.fromUser(user));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||||
|
// 清理上下文
|
||||||
|
UserContext.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractToken(HttpServletRequest request) {
|
||||||
|
String bearerToken = request.getHeader("Authorization");
|
||||||
|
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||||
|
return bearerToken.substring(7);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeUnauthorizedResponse(HttpServletResponse response, String message) throws Exception {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
R<?> result = R.fail(401, message);
|
||||||
|
response.getWriter().write(objectMapper.writeValueAsString(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.tailbet.config;
|
||||||
|
|
||||||
|
import com.tailbet.model.vo.R;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局异常处理
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public R<?> handleRuntimeException(RuntimeException e) {
|
||||||
|
log.error("业务异常: {}", e.getMessage());
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public R<?> handleException(Exception e) {
|
||||||
|
log.error("系统异常: {}", e.getMessage(), e);
|
||||||
|
return R.fail("系统错误: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.tailbet.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "openim")
|
||||||
|
public class OpenIMConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM API地址
|
||||||
|
*/
|
||||||
|
private String apiUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM WebSocket地址
|
||||||
|
*/
|
||||||
|
private String wsUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员账号
|
||||||
|
*/
|
||||||
|
private String adminUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员Token
|
||||||
|
*/
|
||||||
|
private String adminToken;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.tailbet.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||||
|
StringRedisTemplate template = new StringRedisTemplate();
|
||||||
|
template.setConnectionFactory(connectionFactory);
|
||||||
|
template.setKeySerializer(new StringRedisSerializer());
|
||||||
|
template.setValueSerializer(new StringRedisSerializer());
|
||||||
|
template.setHashKeySerializer(new StringRedisSerializer());
|
||||||
|
template.setHashValueSerializer(new StringRedisSerializer());
|
||||||
|
template.afterPropertiesSet();
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.tailbet.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RestTemplate配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate() {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.tailbet.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebMvc配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
private final AuthInterceptor authInterceptor;
|
||||||
|
|
||||||
|
public WebMvcConfig(AuthInterceptor authInterceptor) {
|
||||||
|
this.authInterceptor = authInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
registry.addMapping("/**")
|
||||||
|
.allowedOriginPatterns("*")
|
||||||
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
|
.allowedHeaders("*")
|
||||||
|
.allowCredentials(true)
|
||||||
|
.maxAge(3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(authInterceptor)
|
||||||
|
.addPathPatterns("/**")
|
||||||
|
.excludePathPatterns("/api/user/register", "/api/user/login");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.tailbet.controller;
|
||||||
|
|
||||||
|
import com.tailbet.model.dto.BetDTO;
|
||||||
|
import com.tailbet.model.dto.GameOperateDTO;
|
||||||
|
import com.tailbet.model.dto.HistoryQueryDTO;
|
||||||
|
import com.tailbet.model.dto.SetDigitDTO;
|
||||||
|
import com.tailbet.model.vo.R;
|
||||||
|
import com.tailbet.model.vo.UserContext;
|
||||||
|
import com.tailbet.service.IBetService;
|
||||||
|
import com.tailbet.service.IGameService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/game")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GameController {
|
||||||
|
|
||||||
|
private final IGameService gameService;
|
||||||
|
private final IBetService betService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏(群主)
|
||||||
|
*/
|
||||||
|
@PostMapping("/start")
|
||||||
|
public R<?> startGame(@RequestBody GameOperateDTO dto) {
|
||||||
|
try {
|
||||||
|
Long userId = UserContext.getUserId();
|
||||||
|
var round = gameService.startGame(dto.getGroupId(), userId);
|
||||||
|
return R.ok(round);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束接注(群主)
|
||||||
|
*/
|
||||||
|
@PostMapping("/end")
|
||||||
|
public R<?> endBet(@RequestBody GameOperateDTO dto) {
|
||||||
|
try {
|
||||||
|
Long userId = UserContext.getUserId();
|
||||||
|
var round = gameService.endBet(dto.getGroupId(), userId);
|
||||||
|
return R.ok(round);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止自动连开
|
||||||
|
*/
|
||||||
|
@PostMapping("/stop")
|
||||||
|
public R<?> stopAutoNext(@RequestBody GameOperateDTO dto) {
|
||||||
|
try {
|
||||||
|
Long userId = UserContext.getUserId();
|
||||||
|
gameService.stopAutoNext(dto.getGroupId(), userId);
|
||||||
|
return R.ok("已停止自动连开");
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下注
|
||||||
|
*/
|
||||||
|
@PostMapping("/bet")
|
||||||
|
public R<?> bet(@RequestBody BetDTO dto) {
|
||||||
|
try {
|
||||||
|
Long userId = UserContext.getUserId();
|
||||||
|
var bet = betService.placeBet(userId, dto.getGroupId(),
|
||||||
|
dto.getClientMsgId(), dto.getPlayType(), dto.getBetAmount());
|
||||||
|
if (bet == null) {
|
||||||
|
return R.ok("假人下注已记录");
|
||||||
|
}
|
||||||
|
return R.ok(bet);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尾数试算
|
||||||
|
*/
|
||||||
|
@GetMapping("/bet/trial")
|
||||||
|
public R<?> trialDigit(Long groupId, Long roundId) {
|
||||||
|
try {
|
||||||
|
List<GameService.DigitTrial> trials = gameService.trialDigit(groupId, roundId);
|
||||||
|
return R.ok(trials);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开奖历史
|
||||||
|
*/
|
||||||
|
@GetMapping("/history")
|
||||||
|
public R<?> history(HistoryQueryDTO query) {
|
||||||
|
var page = gameService.getHistory(query.getGroupId(), query.getPageNum(), query.getPageSize());
|
||||||
|
return R.ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置尾数(白名单用户)
|
||||||
|
*/
|
||||||
|
@PostMapping("/digit/set")
|
||||||
|
public R<?> setDigit(@RequestBody SetDigitDTO dto) {
|
||||||
|
try {
|
||||||
|
Long userId = UserContext.getUserId();
|
||||||
|
gameService.setTargetDigit(dto.getGroupId(), dto.getDigit(), userId);
|
||||||
|
return R.ok("尾数已设置");
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.tailbet.controller;
|
||||||
|
|
||||||
|
import com.tailbet.model.dto.PointsDTO;
|
||||||
|
import com.tailbet.model.dto.PullGroupDTO;
|
||||||
|
import com.tailbet.model.vo.R;
|
||||||
|
import com.tailbet.model.vo.UserContext;
|
||||||
|
import com.tailbet.service.IPointsService;
|
||||||
|
import com.tailbet.service.IGroupService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/ops")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OpsController {
|
||||||
|
|
||||||
|
private final IPointsService pointsService;
|
||||||
|
private final IGroupService groupService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加积分
|
||||||
|
*/
|
||||||
|
@PostMapping("/points/add")
|
||||||
|
public R<?> addPoints(@RequestBody PointsDTO dto) {
|
||||||
|
try {
|
||||||
|
Long operatorId = UserContext.getUserId();
|
||||||
|
pointsService.addPoints(dto.getUserId(), dto.getAmount(),
|
||||||
|
IPointsService.TYPE_OPS_ADD, null, operatorId, dto.getRemark());
|
||||||
|
return R.ok("加分成功");
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 减积分
|
||||||
|
*/
|
||||||
|
@PostMapping("/points/sub")
|
||||||
|
public R<?> subPoints(@RequestBody PointsDTO dto) {
|
||||||
|
try {
|
||||||
|
Long operatorId = UserContext.getUserId();
|
||||||
|
boolean success = pointsService.subPoints(dto.getUserId(), dto.getAmount(),
|
||||||
|
IPointsService.TYPE_OPS_SUB, null, operatorId, dto.getRemark());
|
||||||
|
if (success) {
|
||||||
|
return R.ok("减分成功");
|
||||||
|
}
|
||||||
|
return R.fail("余额不足或用户不存在");
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉入群
|
||||||
|
*/
|
||||||
|
@PostMapping("/group/pull")
|
||||||
|
public R<?> pullGroup(@RequestBody PullGroupDTO dto) {
|
||||||
|
try {
|
||||||
|
Long operatorId = UserContext.getUserId();
|
||||||
|
groupService.pullUser(dto.getGroupId(), dto.getUserId(), operatorId);
|
||||||
|
return R.ok("拉群成功");
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.tailbet.controller;
|
||||||
|
|
||||||
|
import com.tailbet.model.dto.ReceiveRpDTO;
|
||||||
|
import com.tailbet.model.dto.SendRpDTO;
|
||||||
|
import com.tailbet.model.vo.R;
|
||||||
|
import com.tailbet.model.vo.UserContext;
|
||||||
|
import com.tailbet.service.IRedPacketService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/rp")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RedPacketController {
|
||||||
|
|
||||||
|
private final IRedPacketService redPacketService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发红包
|
||||||
|
*/
|
||||||
|
@PostMapping("/send")
|
||||||
|
public R<?> sendRp(@RequestBody SendRpDTO dto) {
|
||||||
|
try {
|
||||||
|
Long userId = UserContext.getUserId();
|
||||||
|
var rp = redPacketService.sendRp(userId, dto.getGroupId(),
|
||||||
|
dto.getTotalAmount(), dto.getTotalCount(), dto.getRoundId());
|
||||||
|
return R.ok(rp);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领红包
|
||||||
|
*/
|
||||||
|
@PostMapping("/receive")
|
||||||
|
public R<?> receiveRp(@RequestBody ReceiveRpDTO dto) {
|
||||||
|
try {
|
||||||
|
Long userId = UserContext.getUserId();
|
||||||
|
var recv = redPacketService.receiveRp(dto.getRpId(), userId);
|
||||||
|
return R.ok(recv);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取红包详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/detail")
|
||||||
|
public R<?> detail(Long rpId) {
|
||||||
|
var rp = redPacketService.getById(rpId);
|
||||||
|
return R.ok(rp);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.tailbet.controller;
|
||||||
|
|
||||||
|
import com.tailbet.model.dto.LoginDTO;
|
||||||
|
import com.tailbet.model.dto.RegisterDTO;
|
||||||
|
import com.tailbet.model.dto.UpdateUserDTO;
|
||||||
|
import com.tailbet.model.vo.LoginUserVo;
|
||||||
|
import com.tailbet.model.vo.R;
|
||||||
|
import com.tailbet.model.vo.UserContext;
|
||||||
|
import com.tailbet.service.IUserService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/user")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final IUserService userService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册
|
||||||
|
*/
|
||||||
|
@PostMapping("/register")
|
||||||
|
public R<?> register(@RequestBody RegisterDTO dto) {
|
||||||
|
try {
|
||||||
|
userService.register(dto.getUsername(), dto.getPassword());
|
||||||
|
return R.ok("注册成功");
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
*/
|
||||||
|
@PostMapping("/login")
|
||||||
|
public R<?> login(@RequestBody LoginDTO dto) {
|
||||||
|
try {
|
||||||
|
String token = userService.login(dto.getUsername(), dto.getPassword());
|
||||||
|
return R.ok("登录成功", java.util.Map.of("token", token));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息
|
||||||
|
*/
|
||||||
|
@GetMapping("/info")
|
||||||
|
public R<?> getInfo() {
|
||||||
|
LoginUserVo user = UserContext.getUser();
|
||||||
|
return R.ok(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
*/
|
||||||
|
@PutMapping("/update")
|
||||||
|
public R<?> update(@RequestBody UpdateUserDTO dto) {
|
||||||
|
try {
|
||||||
|
Long userId = UserContext.getUserId();
|
||||||
|
userService.updateUserInfo(userId, dto);
|
||||||
|
return R.ok("更新成功");
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public R<?> logout() {
|
||||||
|
// 可以在这里清理Redis中的token
|
||||||
|
return R.ok("退出成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package com.tailbet.job;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.GameRound;
|
||||||
|
import com.tailbet.model.entity.RedPacket;
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.tailbet.mapper.GameRoundMapper;
|
||||||
|
import com.tailbet.mapper.RedPacketMapper;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.mapper.DigitWhiteMapper;
|
||||||
|
import com.tailbet.model.entity.DigitWhite;
|
||||||
|
import com.tailbet.service.GameService;
|
||||||
|
import com.tailbet.service.RedPacketService;
|
||||||
|
import com.tailbet.service.FakeUserService;
|
||||||
|
import com.tailbet.openim.OpenIMClient;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏定时任务
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GameJob {
|
||||||
|
|
||||||
|
private final GameRoundMapper gameRoundMapper;
|
||||||
|
private final RedPacketMapper redPacketMapper;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final DigitWhiteMapper digitWhiteMapper;
|
||||||
|
private final GameService gameService;
|
||||||
|
private final RedPacketService redPacketService;
|
||||||
|
private final FakeUserService fakeUserService;
|
||||||
|
private final OpenIMClient openIMClient;
|
||||||
|
|
||||||
|
// 局状态监控(使用内存缓存,实际生产应使用Redis)
|
||||||
|
private final Map<Long, RoundTask> roundTasks = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查红包是否需要托领取(每3秒执行)
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 3000)
|
||||||
|
public void checkRedPacketForFakeReceive() {
|
||||||
|
// 查找进行中的游戏红包
|
||||||
|
List<RedPacket> ongoingRps = redPacketMapper.selectList(new LambdaQueryWrapper<RedPacket>()
|
||||||
|
.eq(RedPacket::getStatus, 0)
|
||||||
|
.eq(RedPacket::getType, RedPacketService.TYPE_GAME)
|
||||||
|
.isNotNull(RedPacket::getRoundId));
|
||||||
|
|
||||||
|
for (RedPacket rp : ongoingRps) {
|
||||||
|
LocalDateTime createTime = rp.getCreateTime();
|
||||||
|
// 发出超过5秒未领完,让托领取
|
||||||
|
if (createTime.plusSeconds(5).isBefore(LocalDateTime.now())) {
|
||||||
|
// 查找未领取的用户,随机选一个托领取
|
||||||
|
try {
|
||||||
|
fakeUserReceiveRemaining(rp);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("托领取红包失败: rpId={}, error={}", rp.getId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 假人托领取剩余份额
|
||||||
|
*/
|
||||||
|
private void fakeUserReceiveRemaining(RedPacket rp) {
|
||||||
|
List<User> fakes = fakeUserService.getFakeUsers();
|
||||||
|
if (fakes.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机选一个假人
|
||||||
|
User fake = fakes.get((int) (Math.random() * fakes.size()));
|
||||||
|
|
||||||
|
// 检查是否已经领取过
|
||||||
|
var existing = redPacketMapper.selectList(new LambdaQueryWrapper<RedPacket>()
|
||||||
|
.eq(RedPacket::getUserId, fake.getId()));
|
||||||
|
if (!existing.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
redPacketService.receiveRp(rp.getId(), fake.getId());
|
||||||
|
log.info("假人{}领取红包{}剩余份额", fake.getId(), rp.getId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略领取失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查局超时(每10秒执行)
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 10000)
|
||||||
|
public void checkRoundTimeout() {
|
||||||
|
List<GameRound> ongoingRounds = gameRoundMapper.selectList(new LambdaQueryWrapper<GameRound>()
|
||||||
|
.eq(GameRound::getStatus, 0));
|
||||||
|
|
||||||
|
for (GameRound round : ongoingRounds) {
|
||||||
|
// 检查是否超过接注时间(默认60秒)
|
||||||
|
RoundTask task = roundTasks.get(round.getId());
|
||||||
|
if (task != null && task.endBetTime.isBefore(LocalDateTime.now())) {
|
||||||
|
// 自动结束接注
|
||||||
|
try {
|
||||||
|
gameService.endBet(round.getGroupId(), 0L); // 0L表示系统自动触发
|
||||||
|
// 触发开始发红包
|
||||||
|
scheduleStartRp(round.getId(), round.getGroupId(), 20); // 20秒后开始发红包
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("自动结束接注失败: roundId={}, error={}", round.getId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每日凌晨重置活跃假人池
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 0 0 * * ?")
|
||||||
|
public void resetDailyFakeUsers() {
|
||||||
|
fakeUserService.resetDailyActiveFakes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排开始发红包
|
||||||
|
*/
|
||||||
|
public void scheduleStartRp(Long roundId, Long groupId, int delaySeconds) {
|
||||||
|
RoundTask task = new RoundTask();
|
||||||
|
task.roundId = roundId;
|
||||||
|
task.groupId = groupId;
|
||||||
|
task.startRpTime = LocalDateTime.now().plusSeconds(delaySeconds);
|
||||||
|
roundTasks.put(roundId, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排自动结算
|
||||||
|
*/
|
||||||
|
public void scheduleSettle(Long roundId, Long groupId, int digit, int delaySeconds) {
|
||||||
|
// TODO: 使用延迟队列或定时任务实现
|
||||||
|
log.info("安排{}秒后结算 roundId={}, digit={}", delaySeconds, roundId, digit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏后安排定时器
|
||||||
|
*/
|
||||||
|
public void onGameStart(Long roundId, Long groupId, int intervalSeconds) {
|
||||||
|
RoundTask task = new RoundTask();
|
||||||
|
task.roundId = roundId;
|
||||||
|
task.groupId = groupId;
|
||||||
|
task.endBetTime = LocalDateTime.now().plusSeconds(intervalSeconds);
|
||||||
|
roundTasks.put(roundId, task);
|
||||||
|
|
||||||
|
// 广播开始下注
|
||||||
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
|
if (round != null) {
|
||||||
|
broadcastStartBet(groupId, round.getRoundNo());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("游戏开始: roundId={}, {}秒后结束接注", roundId, intervalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束接注后安排发红包
|
||||||
|
*/
|
||||||
|
public void onEndBet(Long roundId, Long groupId) {
|
||||||
|
// 移除结束接注定时器
|
||||||
|
roundTasks.remove(roundId);
|
||||||
|
|
||||||
|
// 广播结束下注
|
||||||
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
|
if (round != null) {
|
||||||
|
broadcastEndBet(groupId, round.getRoundNo());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知白名单用户尾数试算完成
|
||||||
|
notifyWhiteUsers(roundId);
|
||||||
|
|
||||||
|
// 安排20秒后开始发红包
|
||||||
|
scheduleStartRp(roundId, groupId, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始发红包
|
||||||
|
*/
|
||||||
|
public void onStartRp(Long roundId, Long groupId) {
|
||||||
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
|
if (round == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 广播开始发红包
|
||||||
|
broadcastStartRp(groupId, round.getRoundNo());
|
||||||
|
|
||||||
|
// 安排10秒后如果没人发包则机器人自动发
|
||||||
|
// TODO: 实现延迟检查逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机器人自动发包
|
||||||
|
*/
|
||||||
|
public void botSendRp(Long roundId, Long groupId) {
|
||||||
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
|
if (round == null || round.getTargetDigit() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找机器人账号
|
||||||
|
User bot = userMapper.selectOne(new LambdaQueryWrapper<User>()
|
||||||
|
.eq(User::getIsBot, 1)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
|
||||||
|
if (bot == null) {
|
||||||
|
log.error("未找到机器人账号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 机器人发1元3个包
|
||||||
|
RedPacket rp = redPacketService.sendRp(bot.getId(), groupId,
|
||||||
|
new BigDecimal("1"), 3, roundId);
|
||||||
|
|
||||||
|
// 设置目标尾数
|
||||||
|
// TODO: 实现红包尾数控制
|
||||||
|
|
||||||
|
log.info("机器人自动发包: roundId={}, rpId={}", roundId, rp.getId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("机器人发包失败: roundId={}, error={}", roundId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开奖结算
|
||||||
|
*/
|
||||||
|
public void onSettle(Long roundId, Long groupId, int digit) {
|
||||||
|
try {
|
||||||
|
gameService.settle(roundId, digit);
|
||||||
|
|
||||||
|
// 广播开奖结果
|
||||||
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
|
if (round != null) {
|
||||||
|
broadcastResult(groupId, digit, round.getRoundNo());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否自动开启下一期
|
||||||
|
GameRound roundAfterSettle = gameRoundMapper.selectById(roundId);
|
||||||
|
if (roundAfterSettle != null && roundAfterSettle.getAutoNext() == 1) {
|
||||||
|
// 5秒后自动开始下一期
|
||||||
|
scheduleAutoNext(groupId, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理任务
|
||||||
|
roundTasks.remove(roundId);
|
||||||
|
|
||||||
|
log.info("局结算完成: roundId={}, digit={}", roundId, digit);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("结算失败: roundId={}, error={}", roundId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知白名单用户(尾数试算完成)
|
||||||
|
*/
|
||||||
|
private void notifyWhiteUsers(Long roundId) {
|
||||||
|
List<DigitWhite> whites = digitWhiteMapper.selectList(new LambdaQueryWrapper<DigitWhite>());
|
||||||
|
for (DigitWhite white : whites) {
|
||||||
|
User user = userMapper.selectById(white.getUserId());
|
||||||
|
if (user != null) {
|
||||||
|
// TODO: 发送推送通知
|
||||||
|
log.info("通知白名单用户尾数试算完成: userId={}", user.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排自动开始下一期
|
||||||
|
*/
|
||||||
|
private void scheduleAutoNext(Long groupId, int delaySeconds) {
|
||||||
|
// TODO: 实现延迟自动开局
|
||||||
|
log.info("安排{}秒后自动开始下一期", delaySeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播开始下注
|
||||||
|
*/
|
||||||
|
private void broadcastStartBet(Long groupId, String roundNo) {
|
||||||
|
// TODO: 获取群对应的OpenIM群ID并发送
|
||||||
|
openIMClient.sendGroupMessage(String.valueOf(groupId),
|
||||||
|
roundNo + "期开始下注");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播结束下注
|
||||||
|
*/
|
||||||
|
private void broadcastEndBet(Long groupId, String roundNo) {
|
||||||
|
openIMClient.sendGroupMessage(String.valueOf(groupId),
|
||||||
|
roundNo + "期结束下注");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播开始发红包
|
||||||
|
*/
|
||||||
|
private void broadcastStartRp(Long groupId, String roundNo) {
|
||||||
|
openIMClient.sendGroupMessage(String.valueOf(groupId),
|
||||||
|
roundNo + "期开始发红包");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播开奖结果
|
||||||
|
*/
|
||||||
|
private void broadcastResult(Long groupId, int digit, String roundNo) {
|
||||||
|
String playWinDesc = gameService.getPlayWinDesc(digit);
|
||||||
|
String message = roundNo + "期开奖\n" +
|
||||||
|
"尾数: " + digit + "\n" +
|
||||||
|
"中奖玩法: " + playWinDesc;
|
||||||
|
openIMClient.sendGroupMessage(String.valueOf(groupId), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 局任务信息
|
||||||
|
*/
|
||||||
|
private static class RoundTask {
|
||||||
|
Long roundId;
|
||||||
|
Long groupId;
|
||||||
|
LocalDateTime endBetTime;
|
||||||
|
LocalDateTime startRpTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.AuditLog;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计日志Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AuditLogMapper extends BaseMapper<AuditLog> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.BetOrder;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注单Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface BetOrderMapper extends BaseMapper<BetOrder> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.DigitWhite;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尾数控制白名单Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DigitWhiteMapper extends BaseMapper<DigitWhite> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.DrawRecord;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开奖记录Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DrawRecordMapper extends BaseMapper<DrawRecord> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.GameRound;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏局Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface GameRoundMapper extends BaseMapper<GameRound> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.Group;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface GroupMapper extends BaseMapper<Group> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.GroupMember;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群成员Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface GroupMemberMapper extends BaseMapper<GroupMember> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.PointsFlow;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分流水Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface PointsFlowMapper extends BaseMapper<PointsFlow> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.RedPacket;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface RedPacketMapper extends BaseMapper<RedPacket> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.RedPacketRecv;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包领取Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface RedPacketRecvMapper extends BaseMapper<RedPacketRecv> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.SysConfig;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统配置Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface SysConfigMapper extends BaseMapper<SysConfig> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.tailbet.mapper;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface UserMapper extends BaseMapper<User> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.tailbet.model;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PageDomain {
|
||||||
|
/**
|
||||||
|
* 页码
|
||||||
|
*/
|
||||||
|
private Integer pageNum = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页显示数量
|
||||||
|
*/
|
||||||
|
private Integer pageSize = 10;
|
||||||
|
|
||||||
|
|
||||||
|
public <T> Page<T> toPage() {
|
||||||
|
return new Page<>(pageNum, pageSize);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下注请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class BetDTO {
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端消息ID(防重)
|
||||||
|
*/
|
||||||
|
private String clientMsgId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩法:单/双/大/小/数字
|
||||||
|
*/
|
||||||
|
private String playType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下注金额
|
||||||
|
*/
|
||||||
|
private Integer betAmount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏操作请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class GameOperateDTO {
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import com.tailbet.model.PageDomain;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 历史查询分页参数
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class HistoryQueryDTO extends PageDomain {
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class LoginDTO {
|
||||||
|
/**
|
||||||
|
* 账号
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码
|
||||||
|
*/
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM回调消息体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class OpenIMCallbackDTO {
|
||||||
|
/**
|
||||||
|
* 消息ID
|
||||||
|
*/
|
||||||
|
private String msgId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送者用户ID
|
||||||
|
*/
|
||||||
|
private String sendId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收者ID(群ID或用户ID)
|
||||||
|
*/
|
||||||
|
private String recvId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型: text, custom, etc
|
||||||
|
*/
|
||||||
|
private String msgType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话类型: 1-单聊, 2-群聊
|
||||||
|
*/
|
||||||
|
private Integer sessionType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群ID(群聊时)
|
||||||
|
*/
|
||||||
|
private String groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端平台
|
||||||
|
*/
|
||||||
|
private String platform;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页请求基类
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PageParam {
|
||||||
|
/**
|
||||||
|
* 页码
|
||||||
|
*/
|
||||||
|
private Integer pageNum = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页显示数量
|
||||||
|
*/
|
||||||
|
private Integer pageSize = 10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分操作请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PointsDTO {
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分数量
|
||||||
|
*/
|
||||||
|
private Long amount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉群请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PullGroupDTO {
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领红包请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ReceiveRpDTO {
|
||||||
|
/**
|
||||||
|
* 红包ID
|
||||||
|
*/
|
||||||
|
private Long rpId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户注册请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RegisterDTO {
|
||||||
|
/**
|
||||||
|
* 账号
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码
|
||||||
|
*/
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发红包请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SendRpDTO {
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包总金额
|
||||||
|
*/
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包个数
|
||||||
|
*/
|
||||||
|
private Integer totalCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定的局ID(可选)
|
||||||
|
*/
|
||||||
|
private Long roundId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置尾数请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SetDigitDTO {
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标尾数(0-9)
|
||||||
|
*/
|
||||||
|
private Integer digit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.tailbet.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UpdateUserDTO {
|
||||||
|
/**
|
||||||
|
* 昵称
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像URL
|
||||||
|
*/
|
||||||
|
private String avatarUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新密码(可选)
|
||||||
|
*/
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计日志表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("audit_log")
|
||||||
|
public class AuditLog {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 局ID
|
||||||
|
*/
|
||||||
|
private Long roundId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动作类型
|
||||||
|
*/
|
||||||
|
private String action;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情JSON
|
||||||
|
*/
|
||||||
|
private String detail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注单表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("bet_order")
|
||||||
|
public class BetOrder {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 局ID
|
||||||
|
*/
|
||||||
|
private Long roundId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端消息ID(防重)
|
||||||
|
*/
|
||||||
|
private String clientMsgId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩法:单/双/大/小/数字
|
||||||
|
*/
|
||||||
|
private String playType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下注金额
|
||||||
|
*/
|
||||||
|
private Integer betAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 赔率
|
||||||
|
*/
|
||||||
|
private BigDecimal odds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否假人下注
|
||||||
|
*/
|
||||||
|
private Integer isFake;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:0待结算 1已结算 2未中奖
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中奖金额
|
||||||
|
*/
|
||||||
|
private Long winAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尾数控制白名单表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("digit_white")
|
||||||
|
public class DigitWhite {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开奖记录表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("draw_record")
|
||||||
|
public class DrawRecord {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 期号
|
||||||
|
*/
|
||||||
|
private String roundNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 局ID
|
||||||
|
*/
|
||||||
|
private Long roundId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开奖尾数:0-9
|
||||||
|
*/
|
||||||
|
private Integer digit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中奖玩法:单,大,数字7
|
||||||
|
*/
|
||||||
|
private String playWins;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总下注
|
||||||
|
*/
|
||||||
|
private Integer totalBet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总派奖
|
||||||
|
*/
|
||||||
|
private Long totalAward;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 庄家利润
|
||||||
|
*/
|
||||||
|
private Long profit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏局表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("game_round")
|
||||||
|
public class GameRound {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 期号:yyyy-MM-dd-xx
|
||||||
|
*/
|
||||||
|
private String roundNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:0进行中 1已结束
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截止下注时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime endBetTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始发红包时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime startRpTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime finishTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标尾数:0-9 null表示待定
|
||||||
|
*/
|
||||||
|
private Integer targetDigit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尾数策略:specified指定/manual_auto小赢小输/random随机
|
||||||
|
*/
|
||||||
|
private String digitStrategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动开启下一期:0否 1是
|
||||||
|
*/
|
||||||
|
private Integer autoNext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("group")
|
||||||
|
public class Group {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM群ID
|
||||||
|
*/
|
||||||
|
private String openimGroupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群主用户ID
|
||||||
|
*/
|
||||||
|
private Long ownerUserId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接注间隔(秒)
|
||||||
|
*/
|
||||||
|
private Integer roundInterval;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:1启用 0停用
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群成员表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("group_member")
|
||||||
|
public class GroupMember {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色:0普通 1管理员 2群主
|
||||||
|
*/
|
||||||
|
private Integer role;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加入时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime joinTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分流水表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("points_flow")
|
||||||
|
public class PointsFlow {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型:ops_add/ops_sub/bet/award/rp_send/rp_recv
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变动数值(正负)
|
||||||
|
*/
|
||||||
|
private Long amount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变动前余额
|
||||||
|
*/
|
||||||
|
private Long balanceBefore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变动后余额
|
||||||
|
*/
|
||||||
|
private Long balanceAfter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务单号
|
||||||
|
*/
|
||||||
|
private String bizNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作人ID
|
||||||
|
*/
|
||||||
|
private Long operatorId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("red_packet")
|
||||||
|
public class RedPacket {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群ID
|
||||||
|
*/
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定局ID(null=普通红包)
|
||||||
|
*/
|
||||||
|
private Long roundId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送者ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型:game游戏/random随机
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总金额
|
||||||
|
*/
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总个数
|
||||||
|
*/
|
||||||
|
private Integer totalCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 剩余金额
|
||||||
|
*/
|
||||||
|
private BigDecimal remainAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 剩余个数
|
||||||
|
*/
|
||||||
|
private Integer remainCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手气王金额
|
||||||
|
*/
|
||||||
|
private BigDecimal luckyAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手气王用户ID
|
||||||
|
*/
|
||||||
|
private Long luckyUserId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手气王尾数
|
||||||
|
*/
|
||||||
|
private Integer luckyDigit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:0进行中 1已领完 2已退款
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包领取表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("red_packet_recv")
|
||||||
|
public class RedPacketRecv {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包ID
|
||||||
|
*/
|
||||||
|
private Long rpId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领取者ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领取金额
|
||||||
|
*/
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否手气王
|
||||||
|
*/
|
||||||
|
private Integer isLucky;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统配置表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("sys_config")
|
||||||
|
public class SysConfig {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置键
|
||||||
|
*/
|
||||||
|
private String cfgKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置值
|
||||||
|
*/
|
||||||
|
private String cfgValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.tailbet.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户表
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("user")
|
||||||
|
public class User {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码(BCrypt)
|
||||||
|
*/
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 昵称
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像URL
|
||||||
|
*/
|
||||||
|
private String avatarUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分余额
|
||||||
|
*/
|
||||||
|
private Long points;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营标记:0否 1是
|
||||||
|
*/
|
||||||
|
private Integer isOps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机器人标记:0否 1是
|
||||||
|
*/
|
||||||
|
private Integer isBot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 假人托标记:0否 1是
|
||||||
|
*/
|
||||||
|
private Integer isFake;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加白(尾数控制)标记
|
||||||
|
*/
|
||||||
|
private Integer isWhite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定的运营用户ID
|
||||||
|
*/
|
||||||
|
private Long boundOpsUserId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:1正常 0封禁
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.tailbet.model.vo;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录用户信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class LoginUserVo {
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 昵称
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像
|
||||||
|
*/
|
||||||
|
private String avatarUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分
|
||||||
|
*/
|
||||||
|
private Long points;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否运营
|
||||||
|
*/
|
||||||
|
private Boolean isOps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否机器人
|
||||||
|
*/
|
||||||
|
private Boolean isBot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否假人
|
||||||
|
*/
|
||||||
|
private Boolean isFake;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否加白
|
||||||
|
*/
|
||||||
|
private Boolean isWhite;
|
||||||
|
|
||||||
|
public static LoginUserVo fromUser(User user) {
|
||||||
|
if (user == null) return null;
|
||||||
|
LoginUserVo vo = new LoginUserVo();
|
||||||
|
vo.setUserId(user.getId());
|
||||||
|
vo.setUsername(user.getUsername());
|
||||||
|
vo.setNickname(user.getNickname());
|
||||||
|
vo.setAvatarUrl(user.getAvatarUrl());
|
||||||
|
vo.setPoints(user.getPoints());
|
||||||
|
vo.setIsOps(user.getIsOps() != null && user.getIsOps() == 1);
|
||||||
|
vo.setIsBot(user.getIsBot() != null && user.getIsBot() == 1);
|
||||||
|
vo.setIsFake(user.getIsFake() != null && user.getIsFake() == 1);
|
||||||
|
vo.setIsWhite(user.getIsWhite() != null && user.getIsWhite() == 1);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.tailbet.model.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一返回对象
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class R<T> implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 成功 */
|
||||||
|
public static final int SUCCESS = 200;
|
||||||
|
|
||||||
|
/** 失败 */
|
||||||
|
public static final int FAIL = 500;
|
||||||
|
|
||||||
|
private int code;
|
||||||
|
private String msg;
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
public static <T> R<T> ok() {
|
||||||
|
return new R<>(SUCCESS, "操作成功", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> R<T> ok(T data) {
|
||||||
|
return new R<>(SUCCESS, "操作成功", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> R<T> ok(String msg, T data) {
|
||||||
|
return new R<>(SUCCESS, msg, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> R<T> fail() {
|
||||||
|
return new R<>(FAIL, "操作失败", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> R<T> fail(String msg) {
|
||||||
|
return new R<>(FAIL, msg, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> R<T> fail(int code, String msg) {
|
||||||
|
return new R<>(code, msg, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return code == SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.tailbet.model.vo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户上下文 - ThreadLocal保存当前登录用户
|
||||||
|
*/
|
||||||
|
public class UserContext {
|
||||||
|
|
||||||
|
private static final ThreadLocal<LoginUserVo> USER = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public static void setUser(LoginUserVo user) {
|
||||||
|
USER.set(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LoginUserVo getUser() {
|
||||||
|
return USER.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Long getUserId() {
|
||||||
|
LoginUserVo user = USER.get();
|
||||||
|
return user != null ? user.getUserId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void remove() {
|
||||||
|
USER.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isOps() {
|
||||||
|
LoginUserVo user = USER.get();
|
||||||
|
return user != null && Boolean.TRUE.equals(user.getIsOps());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBot() {
|
||||||
|
LoginUserVo user = USER.get();
|
||||||
|
return user != null && Boolean.TRUE.equals(user.getIsBot());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isFake() {
|
||||||
|
LoginUserVo user = USER.get();
|
||||||
|
return user != null && Boolean.TRUE.equals(user.getIsFake());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isWhite() {
|
||||||
|
LoginUserVo user = USER.get();
|
||||||
|
return user != null && Boolean.TRUE.equals(user.getIsWhite());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package com.tailbet.openim;
|
||||||
|
|
||||||
|
import com.tailbet.model.dto.OpenIMCallbackDTO;
|
||||||
|
import com.tailbet.model.vo.R;
|
||||||
|
import com.tailbet.service.BetService;
|
||||||
|
import com.tailbet.service.GameService;
|
||||||
|
import com.tailbet.service.RedPacketService;
|
||||||
|
import com.tailbet.service.GroupService;
|
||||||
|
import com.tailbet.openim.OpenIMClient;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM回调接口
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/openim/callback")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OpenIMCallbackController {
|
||||||
|
|
||||||
|
private final BetService betService;
|
||||||
|
private final GameService gameService;
|
||||||
|
private final RedPacketService redPacketService;
|
||||||
|
private final GroupService groupService;
|
||||||
|
private final OpenIMClient openIMClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收群消息回调
|
||||||
|
*/
|
||||||
|
@PostMapping("/group_msg")
|
||||||
|
public R<?> onGroupMessage(@RequestBody OpenIMCallbackDTO dto) {
|
||||||
|
try {
|
||||||
|
log.info("收到群消息: msgId={}, sendId={}, groupId={}, content={}",
|
||||||
|
dto.getMsgId(), dto.getSendId(), dto.getGroupId(), dto.getContent());
|
||||||
|
|
||||||
|
String content = dto.getContent();
|
||||||
|
if (content == null || content.isEmpty()) {
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理@机器人指令
|
||||||
|
if (content.contains("@机器人") || content.contains("@robot")) {
|
||||||
|
handleBotCommand(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理群消息回调失败: {}", e.getMessage(), e);
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收单聊消息回调
|
||||||
|
*/
|
||||||
|
@PostMapping("/single_msg")
|
||||||
|
public R<?> onSingleMessage(@RequestBody OpenIMCallbackDTO dto) {
|
||||||
|
try {
|
||||||
|
log.info("收到单聊消息: msgId={}, sendId={}, recvId={}, content={}",
|
||||||
|
dto.getMsgId(), dto.getSendId(), dto.getRecvId(), dto.getContent());
|
||||||
|
return R.ok();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理单聊消息回调失败: {}", e.getMessage(), e);
|
||||||
|
return R.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理机器人指令
|
||||||
|
*/
|
||||||
|
private void handleBotCommand(OpenIMCallbackDTO dto) {
|
||||||
|
String content = dto.getContent();
|
||||||
|
String groupId = dto.getGroupId();
|
||||||
|
String sendId = dto.getSendId();
|
||||||
|
|
||||||
|
// 解析指令: @机器人 单 100 或 @机器人 大 50
|
||||||
|
String message = content.replace("@机器人", "").replace("@robot", "").trim();
|
||||||
|
|
||||||
|
if (message.startsWith("统计")) {
|
||||||
|
// 统计指令
|
||||||
|
handleStatCommand(groupId);
|
||||||
|
} else {
|
||||||
|
// 下注指令
|
||||||
|
handleBetCommand(message, groupId, sendId, dto.getMsgId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理下注指令
|
||||||
|
*/
|
||||||
|
private void handleBetCommand(String message, String groupId, String sendId, String msgId) {
|
||||||
|
try {
|
||||||
|
// 格式: 单 100, 双 50, 大 100, 小 50, 7 20
|
||||||
|
String[] parts = message.trim().split("\\s+");
|
||||||
|
if (parts.length != 2) {
|
||||||
|
sendGroupMessage(groupId, "格式错误,请使用: @机器人 单/双/大/小/数字 金额");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String playType = parts[0];
|
||||||
|
Integer betAmount;
|
||||||
|
try {
|
||||||
|
betAmount = Integer.parseInt(parts[1]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
sendGroupMessage(groupId, "金额格式错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证玩法
|
||||||
|
if (!isValidPlayType(playType)) {
|
||||||
|
sendGroupMessage(groupId, "玩法错误,请使用: 单/双/大/小/数字(0-9)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取业务groupId
|
||||||
|
Long bizGroupId = getBizGroupId(groupId);
|
||||||
|
if (bizGroupId == null) {
|
||||||
|
sendGroupMessage(groupId, "群未配置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行下注
|
||||||
|
Long userId = getBizUserId(sendId);
|
||||||
|
var bet = betService.placeBet(userId, bizGroupId, msgId, playType, betAmount);
|
||||||
|
|
||||||
|
if (bet != null) {
|
||||||
|
sendGroupMessage(groupId, String.format("已记录: %s %d", playType, betAmount));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
sendGroupMessage(groupId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理统计指令
|
||||||
|
*/
|
||||||
|
private void handleStatCommand(String groupId) {
|
||||||
|
try {
|
||||||
|
// TODO: 返回统计信息
|
||||||
|
sendGroupMessage(groupId, "统计功能开发中...");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理统计指令失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送群消息
|
||||||
|
*/
|
||||||
|
private void sendGroupMessage(String openimGroupId, String message) {
|
||||||
|
try {
|
||||||
|
openIMClient.sendGroupMessage(openimGroupId, message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送群消息失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证玩法是否有效
|
||||||
|
*/
|
||||||
|
private boolean isValidPlayType(String playType) {
|
||||||
|
return "单".equals(playType) || "双".equals(playType)
|
||||||
|
|| "大".equals(playType) || "小".equals(playType)
|
||||||
|
|| playType.matches("[0-9]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将OpenIM用户ID转换为业务用户ID
|
||||||
|
* 这里需要根据实际实现来转换
|
||||||
|
*/
|
||||||
|
private Long getBizUserId(String openimUserId) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(openimUserId);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// 如果不是数字,需要通过映射表查询
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将OpenIM群ID转换为业务群ID
|
||||||
|
*/
|
||||||
|
private Long getBizGroupId(String openimGroupId) {
|
||||||
|
// TODO: 通过映射表查询
|
||||||
|
return 1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.tailbet.openim;
|
||||||
|
|
||||||
|
import com.tailbet.config.OpenIMConfig;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenIM客户端
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OpenIMClient {
|
||||||
|
|
||||||
|
private final OpenIMConfig config;
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送群消息
|
||||||
|
*/
|
||||||
|
public void sendGroupMessage(String groupId, String message) {
|
||||||
|
String url = config.getApiUrl() + "/msg/send_msg";
|
||||||
|
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("groupId", groupId);
|
||||||
|
body.put("msg", message);
|
||||||
|
body.put("msgType", "text");
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("token", config.getAdminToken());
|
||||||
|
|
||||||
|
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
restTemplate.postForEntity(url, request, String.class);
|
||||||
|
log.info("OpenIM群消息发送成功: groupId={}, message={}", groupId, message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("OpenIM群消息发送失败: groupId={}, message={}, error={}",
|
||||||
|
groupId, message, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送单聊消息
|
||||||
|
*/
|
||||||
|
public void sendUserMessage(String toUserId, String message) {
|
||||||
|
String url = config.getApiUrl() + "/msg/send_msg";
|
||||||
|
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("to", toUserId);
|
||||||
|
body.put("msg", message);
|
||||||
|
body.put("msgType", "text");
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("token", config.getAdminToken());
|
||||||
|
|
||||||
|
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
restTemplate.postForEntity(url, request, String.class);
|
||||||
|
log.info("OpenIM单聊消息发送成功: to={}, message={}", toUserId, message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("OpenIM单聊消息发送失败: to={}, message={}, error={}",
|
||||||
|
toUserId, message, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建用户
|
||||||
|
*/
|
||||||
|
public boolean createUser(String userId, String nickname) {
|
||||||
|
String url = config.getApiUrl() + "/user/user_register";
|
||||||
|
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("userID", userId);
|
||||||
|
body.put("nickname", nickname);
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("token", config.getAdminToken());
|
||||||
|
|
||||||
|
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
restTemplate.postForEntity(url, request, String.class);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建OpenIM用户失败: userId={}, error={}", userId, e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建群组
|
||||||
|
*/
|
||||||
|
public String createGroup(String groupName) {
|
||||||
|
String url = config.getApiUrl() + "/group/create_group";
|
||||||
|
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("groupName", groupName);
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("token", config.getAdminToken());
|
||||||
|
|
||||||
|
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ResponseEntity<Map> response = restTemplate.postForEntity(url, request, Map.class);
|
||||||
|
if (response.getBody() != null) {
|
||||||
|
return (String) response.getBody().get("groupID");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建OpenIM群组失败: groupName={}, error={}", groupName, e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉用户入群
|
||||||
|
*/
|
||||||
|
public boolean inviteUserToGroup(String groupId, String userId) {
|
||||||
|
String url = config.getApiUrl() + "/group/invite_user_to_group";
|
||||||
|
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("groupId", groupId);
|
||||||
|
body.put("userIDs", new String[]{userId});
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("token", config.getAdminToken());
|
||||||
|
|
||||||
|
HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
restTemplate.postForEntity(url, request, String.class);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("拉用户入群失败: groupId={}, userId={}, error={}",
|
||||||
|
groupId, userId, e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.tailbet.service;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.AuditLog;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计日志Service接口
|
||||||
|
*/
|
||||||
|
public interface IAuditService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录审计日志
|
||||||
|
*/
|
||||||
|
void log(Long groupId, Long roundId, Long userId, String action, String detail);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询审计日志
|
||||||
|
*/
|
||||||
|
Page<AuditLog> getPage(Long groupId, Long roundId, String action,
|
||||||
|
Integer pageNum, Integer pageSize);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.tailbet.service;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.BetOrder;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注单Service接口
|
||||||
|
*/
|
||||||
|
public interface IBetService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下注
|
||||||
|
*/
|
||||||
|
BetOrder placeBet(Long userId, Long groupId, String clientMsgId,
|
||||||
|
String playType, Integer betAmount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取赔率
|
||||||
|
*/
|
||||||
|
BigDecimal getOdds(String playType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户本局下注总额
|
||||||
|
*/
|
||||||
|
Integer getUserRoundBetAmount(Long userId, Long roundId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.tailbet.service;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 假人服务接口
|
||||||
|
*/
|
||||||
|
public interface IFakeUserService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化群假人
|
||||||
|
*/
|
||||||
|
void initGroupFakeUsers(Long groupId, int minCount, int maxCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建单个假人
|
||||||
|
*/
|
||||||
|
User createFakeUser(Long groupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每日重置活跃假人
|
||||||
|
*/
|
||||||
|
void resetDailyActiveFakes();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否是今日活跃假人
|
||||||
|
*/
|
||||||
|
boolean isTodayActiveFake(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 假人下注
|
||||||
|
*/
|
||||||
|
void fakeBet(Long groupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取假人列表
|
||||||
|
*/
|
||||||
|
List<User> getFakeUsers();
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.tailbet.service;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.GameRound;
|
||||||
|
import com.tailbet.model.entity.DrawRecord;
|
||||||
|
import com.tailbet.model.entity.BetOrder;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏Service接口
|
||||||
|
*/
|
||||||
|
public interface IGameService {
|
||||||
|
|
||||||
|
// 局状态常量
|
||||||
|
int STATUS_ONGOING = 0;
|
||||||
|
int STATUS_ENDED = 1;
|
||||||
|
|
||||||
|
// 玩法赔率
|
||||||
|
BigDecimal ODDS_SINGLE_DOUBLE = new BigDecimal("1");
|
||||||
|
BigDecimal ODDS_SIZE = new BigDecimal("1");
|
||||||
|
BigDecimal ODDS_DIGIT = new BigDecimal("2");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尾数试算结果
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
class DigitTrial {
|
||||||
|
private int digit;
|
||||||
|
private String playWin;
|
||||||
|
private int totalBet;
|
||||||
|
private long totalAward;
|
||||||
|
private long profit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群当前进行中的局
|
||||||
|
*/
|
||||||
|
GameRound getOngoingRound(Long groupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成期号
|
||||||
|
*/
|
||||||
|
String generateRoundNo(Long groupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏(群主操作)
|
||||||
|
*/
|
||||||
|
GameRound startGame(Long groupId, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束接注
|
||||||
|
*/
|
||||||
|
GameRound endBet(Long groupId, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止自动连开
|
||||||
|
*/
|
||||||
|
void stopAutoNext(Long groupId, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置目标尾数
|
||||||
|
*/
|
||||||
|
void setTargetDigit(Long groupId, Integer digit, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尾数试算
|
||||||
|
*/
|
||||||
|
List<DigitTrial> trialDigit(Long groupId, Long roundId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判定是否中奖
|
||||||
|
*/
|
||||||
|
boolean isWin(BetOrder bet, int digit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取中奖玩法描述
|
||||||
|
*/
|
||||||
|
String getPlayWinDesc(int digit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开奖结算
|
||||||
|
*/
|
||||||
|
void settle(Long roundId, int digit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取开奖历史
|
||||||
|
*/
|
||||||
|
Page<DrawRecord> getHistory(Long groupId, Integer pageNum, Integer pageSize);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.tailbet.service;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.Group;
|
||||||
|
import com.tailbet.model.entity.GroupMember;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群Service接口
|
||||||
|
*/
|
||||||
|
public interface IGroupService {
|
||||||
|
|
||||||
|
// 角色
|
||||||
|
int ROLE_NORMAL = 0;
|
||||||
|
int ROLE_ADMIN = 1;
|
||||||
|
int ROLE_OWNER = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群信息
|
||||||
|
*/
|
||||||
|
Group getById(Long groupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的群列表
|
||||||
|
*/
|
||||||
|
List<Group> getUserGroups(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉用户入群
|
||||||
|
*/
|
||||||
|
void pullUser(Long groupId, Long userId, Long operatorId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 踢出用户
|
||||||
|
*/
|
||||||
|
void kickUser(Long groupId, Long targetUserId, Long operatorId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置管理员
|
||||||
|
*/
|
||||||
|
void setAdmin(Long groupId, Long targetUserId, Long operatorId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消管理员
|
||||||
|
*/
|
||||||
|
void removeAdmin(Long groupId, Long targetUserId, Long operatorId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群成员
|
||||||
|
*/
|
||||||
|
List<GroupMember> getMembers(Long groupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否是群主
|
||||||
|
*/
|
||||||
|
boolean isOwner(Long groupId, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否是管理员
|
||||||
|
*/
|
||||||
|
boolean isAdmin(Long groupId, Long userId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.tailbet.service;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.PointsFlow;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分Service接口
|
||||||
|
*/
|
||||||
|
public interface IPointsService {
|
||||||
|
|
||||||
|
// 积分类型常量
|
||||||
|
String TYPE_OPS_ADD = "ops_add";
|
||||||
|
String TYPE_OPS_SUB = "ops_sub";
|
||||||
|
String TYPE_BET = "bet";
|
||||||
|
String TYPE_AWARD = "award";
|
||||||
|
String TYPE_RP_SEND = "rp_send";
|
||||||
|
String TYPE_RP_RECV = "rp_recv";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加积分
|
||||||
|
*/
|
||||||
|
void addPoints(Long userId, Long amount, String type, String bizNo, Long operatorId, String remark);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扣除积分
|
||||||
|
*/
|
||||||
|
boolean subPoints(Long userId, Long amount, String type, String bizNo, Long operatorId, String remark);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户积分
|
||||||
|
*/
|
||||||
|
Long getPoints(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询积分流水记录
|
||||||
|
*/
|
||||||
|
Page<PointsFlow> getFlowPage(Long userId, Integer pageNum, Integer pageSize);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.tailbet.service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送服务接口
|
||||||
|
*/
|
||||||
|
public interface IPushService {
|
||||||
|
|
||||||
|
// 推送类型常量
|
||||||
|
String PUSH_TYPE_DIGIT_TRIAL = "digit_trial"; // 尾数试算完成
|
||||||
|
String PUSH_TYPE_GAME_START = "game_start"; // 游戏开始
|
||||||
|
String PUSH_TYPE_GAME_END = "game_end"; // 游戏结束
|
||||||
|
String PUSH_TYPE_RESULT = "result"; // 开奖结果
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送尾数试算完成通知给白名单用户
|
||||||
|
*/
|
||||||
|
void pushDigitTrialComplete(Long groupId, Long roundId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送游戏开始通知
|
||||||
|
*/
|
||||||
|
void pushGameStart(Long groupId, String roundNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送开奖结果通知
|
||||||
|
*/
|
||||||
|
void pushGameResult(Long groupId, String roundNo, int digit, String playWins);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送单用户推送
|
||||||
|
*/
|
||||||
|
void sendPush(PushService.PushMessage message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播推送消息给群成员
|
||||||
|
*/
|
||||||
|
void broadcastToGroup(Long groupId, PushService.PushMessage message);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.tailbet.service;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.RedPacket;
|
||||||
|
import com.tailbet.model.entity.RedPacketRecv;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包Service接口
|
||||||
|
*/
|
||||||
|
public interface IRedPacketService {
|
||||||
|
|
||||||
|
// 红包状态
|
||||||
|
int STATUS_ONGOING = 0;
|
||||||
|
int STATUS_FINISHED = 1;
|
||||||
|
int STATUS_REFUND = 2;
|
||||||
|
|
||||||
|
// 红包类型
|
||||||
|
String TYPE_GAME = "game";
|
||||||
|
String TYPE_RANDOM = "random";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发红包
|
||||||
|
*/
|
||||||
|
RedPacket sendRp(Long userId, Long groupId, BigDecimal totalAmount,
|
||||||
|
Integer totalCount, Long roundId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领红包
|
||||||
|
*/
|
||||||
|
RedPacketRecv receiveRp(Long rpId, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算领取金额
|
||||||
|
*/
|
||||||
|
BigDecimal calculateReceiveAmount(RedPacket rp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取红包详情
|
||||||
|
*/
|
||||||
|
RedPacket getById(Long rpId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取红包领取列表
|
||||||
|
*/
|
||||||
|
List<RedPacketRecv> getReceives(Long rpId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询红包记录
|
||||||
|
*/
|
||||||
|
Page<RedPacket> getRecords(Long userId, Integer pageNum, Integer pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找进行中的游戏红包
|
||||||
|
*/
|
||||||
|
RedPacket getOngoingGameRp(Long groupId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.tailbet.service;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户Service接口
|
||||||
|
*/
|
||||||
|
public interface IUserService extends IService<User> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名查询用户
|
||||||
|
*/
|
||||||
|
User getByUsername(String username);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册用户
|
||||||
|
*/
|
||||||
|
User register(String username, String password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
*/
|
||||||
|
String login(String username, String password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户
|
||||||
|
*/
|
||||||
|
User getCurrentUser();
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.tailbet.service.impl;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.AuditLog;
|
||||||
|
import com.tailbet.mapper.AuditLogMapper;
|
||||||
|
import com.tailbet.service.IAuditService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计日志Service实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class AuditServiceImpl implements IAuditService {
|
||||||
|
|
||||||
|
private final AuditLogMapper auditLogMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录审计日志
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void log(Long groupId, Long roundId, Long userId, String action, String detail) {
|
||||||
|
AuditLog auditLog = new AuditLog();
|
||||||
|
auditLog.setGroupId(groupId);
|
||||||
|
auditLog.setRoundId(roundId);
|
||||||
|
auditLog.setUserId(userId);
|
||||||
|
auditLog.setAction(action);
|
||||||
|
auditLog.setDetail(detail);
|
||||||
|
auditLog.setCreateTime(LocalDateTime.now());
|
||||||
|
auditLogMapper.insert(auditLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询审计日志
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<AuditLog> getPage(Long groupId, Long roundId, String action,
|
||||||
|
Integer pageNum, Integer pageSize) {
|
||||||
|
Page<AuditLog> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<AuditLog> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (groupId != null) {
|
||||||
|
wrapper.eq(AuditLog::getGroupId, groupId);
|
||||||
|
}
|
||||||
|
if (roundId != null) {
|
||||||
|
wrapper.eq(AuditLog::getRoundId, roundId);
|
||||||
|
}
|
||||||
|
if (action != null) {
|
||||||
|
wrapper.eq(AuditLog::getAction, action);
|
||||||
|
}
|
||||||
|
wrapper.orderByDesc(AuditLog::getCreateTime);
|
||||||
|
return auditLogMapper.selectPage(page, wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package com.tailbet.service.impl;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.BetOrder;
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.tailbet.model.entity.GameRound;
|
||||||
|
import com.tailbet.mapper.BetOrderMapper;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.mapper.GameRoundMapper;
|
||||||
|
import com.tailbet.service.IBetService;
|
||||||
|
import com.tailbet.service.IGameService;
|
||||||
|
import com.tailbet.service.IPointsService;
|
||||||
|
import com.tailbet.service.IAuditService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
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.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注单Service实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class BetServiceImpl implements IBetService {
|
||||||
|
|
||||||
|
private final BetOrderMapper betOrderMapper;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final GameRoundMapper gameRoundMapper;
|
||||||
|
private final IPointsService pointsService;
|
||||||
|
private final IAuditService auditService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下注
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public BetOrder placeBet(Long userId, Long groupId, String clientMsgId,
|
||||||
|
String playType, Integer betAmount) {
|
||||||
|
// 防重检查
|
||||||
|
if (clientMsgId != null) {
|
||||||
|
BetOrder existing = betOrderMapper.selectOne(new LambdaQueryWrapper<BetOrder>()
|
||||||
|
.eq(BetOrder::getClientMsgId, clientMsgId));
|
||||||
|
if (existing != null) {
|
||||||
|
throw new RuntimeException("重复下注");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户
|
||||||
|
User user = userMapper.selectById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new RuntimeException("用户不存在");
|
||||||
|
}
|
||||||
|
if (user.getIsFake() != null && user.getIsFake() == 1) {
|
||||||
|
// 假人下注只记录审计日志,不产生订单
|
||||||
|
auditService.log(groupId, null, userId, "FAKE_BET",
|
||||||
|
String.format("{\"playType\":\"%s\",\"amount\":%d}", playType, betAmount));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查进行中的局
|
||||||
|
GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
|
||||||
|
.eq(GameRound::getGroupId, groupId)
|
||||||
|
.eq(GameRound::getStatus, 0));
|
||||||
|
if (round == null) {
|
||||||
|
throw new RuntimeException("当前没有进行中的局");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查余额
|
||||||
|
if (user.getPoints() < betAmount) {
|
||||||
|
throw new RuntimeException("余额不足");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查限额
|
||||||
|
if (betAmount < 1 || betAmount > 2000) {
|
||||||
|
throw new RuntimeException("单笔下注金额需在1-2000之间");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查单局累计
|
||||||
|
Integer roundTotal = betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>()
|
||||||
|
.eq(BetOrder::getUserId, userId)
|
||||||
|
.eq(BetOrder::getRoundId, round.getId())
|
||||||
|
.eq(BetOrder::getIsFake, 0))
|
||||||
|
.stream()
|
||||||
|
.mapToInt(BetOrder::getBetAmount)
|
||||||
|
.sum();
|
||||||
|
if (roundTotal + betAmount > 20000) {
|
||||||
|
throw new RuntimeException("单局累计下注不能超过20000");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取赔率
|
||||||
|
BigDecimal odds = getOdds(playType);
|
||||||
|
|
||||||
|
// 扣减积分
|
||||||
|
boolean success = pointsService.subPoints(userId, betAmount.longValue(),
|
||||||
|
IPointsService.TYPE_BET, null, null, "游戏下注");
|
||||||
|
if (!success) {
|
||||||
|
throw new RuntimeException("余额不足");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建注单
|
||||||
|
BetOrder bet = new BetOrder();
|
||||||
|
bet.setUserId(userId);
|
||||||
|
bet.setGroupId(groupId);
|
||||||
|
bet.setRoundId(round.getId());
|
||||||
|
bet.setClientMsgId(clientMsgId);
|
||||||
|
bet.setPlayType(playType);
|
||||||
|
bet.setBetAmount(betAmount);
|
||||||
|
bet.setOdds(odds);
|
||||||
|
bet.setIsFake(0);
|
||||||
|
bet.setStatus(0);
|
||||||
|
bet.setCreateTime(LocalDateTime.now());
|
||||||
|
betOrderMapper.insert(bet);
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(groupId, round.getId(), userId, "BET_PLACE",
|
||||||
|
String.format("{\"betId\":%d,\"playType\":\"%s\",\"amount\":%d}",
|
||||||
|
bet.getId(), playType, betAmount));
|
||||||
|
|
||||||
|
return bet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取赔率
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public BigDecimal getOdds(String playType) {
|
||||||
|
if ("单".equals(playType) || "双".equals(playType)) {
|
||||||
|
return IGameService.ODDS_SINGLE_DOUBLE;
|
||||||
|
} else if ("大".equals(playType) || "小".equals(playType)) {
|
||||||
|
return IGameService.ODDS_SIZE;
|
||||||
|
} else {
|
||||||
|
return IGameService.ODDS_DIGIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户本局下注总额
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Integer getUserRoundBetAmount(Long userId, Long roundId) {
|
||||||
|
return betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>()
|
||||||
|
.eq(BetOrder::getUserId, userId)
|
||||||
|
.eq(BetOrder::getRoundId, roundId)
|
||||||
|
.eq(BetOrder::getIsFake, 0))
|
||||||
|
.stream()
|
||||||
|
.mapToInt(BetOrder::getBetAmount)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.tailbet.service.impl;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.tailbet.model.entity.GameRound;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.mapper.GameRoundMapper;
|
||||||
|
import com.tailbet.service.IFakeUserService;
|
||||||
|
import com.tailbet.service.IBetService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 假人服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class FakeUserServiceImpl implements IFakeUserService {
|
||||||
|
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final GameRoundMapper gameRoundMapper;
|
||||||
|
private final IBetService betService;
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
private static final String FAKE_ACTIVE_KEY = "fake:active:";
|
||||||
|
private static final Random RANDOM = new Random();
|
||||||
|
|
||||||
|
// 假人昵称素材
|
||||||
|
private static final String[] NICKNAME_PREFIXES = {"小", "老", "阿", "超", "酷"};
|
||||||
|
private static final String[] NICKNAME_SUFFIXES = {"哥", "弟", "姐", "妹", "宝"};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化群假人
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void initGroupFakeUsers(Long groupId, int minCount, int maxCount) {
|
||||||
|
int count = minCount + RANDOM.nextInt(maxCount - minCount + 1);
|
||||||
|
log.info("为群{}初始化{}个假人", groupId, count);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
createFakeUser(groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建单个假人
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public User createFakeUser(Long groupId) {
|
||||||
|
User fake = new User();
|
||||||
|
fake.setUsername("fake_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12));
|
||||||
|
fake.setNickname(generateNickname());
|
||||||
|
fake.setPoints((long) (1000 + RANDOM.nextInt(49000)));
|
||||||
|
fake.setIsFake(1);
|
||||||
|
fake.setIsBot(0);
|
||||||
|
fake.setIsOps(0);
|
||||||
|
fake.setIsWhite(0);
|
||||||
|
fake.setStatus(1);
|
||||||
|
userMapper.insert(fake);
|
||||||
|
return fake;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机昵称
|
||||||
|
*/
|
||||||
|
private String generateNickname() {
|
||||||
|
String prefix = NICKNAME_PREFIXES[RANDOM.nextInt(NICKNAME_PREFIXES.length)];
|
||||||
|
String suffix = NICKNAME_SUFFIXES[RANDOM.nextInt(NICKNAME_SUFFIXES.length)];
|
||||||
|
int number = RANDOM.nextInt(1000);
|
||||||
|
return prefix + suffix + number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每日重置活跃假人
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Scheduled(cron = "0 0 0 * * ?")
|
||||||
|
public void resetDailyActiveFakes() {
|
||||||
|
// 清理昨天的活跃标记
|
||||||
|
Set<String> keys = redisTemplate.opsForSet().members(FAKE_ACTIVE_KEY + "yesterday");
|
||||||
|
if (keys != null) {
|
||||||
|
for (String userId : keys) {
|
||||||
|
redisTemplate.opsForSet().remove(FAKE_ACTIVE_KEY + "yesterday", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询所有假人
|
||||||
|
List<User> fakes = userMapper.selectList(new LambdaQueryWrapper<User>()
|
||||||
|
.eq(User::getIsFake, 1)
|
||||||
|
.eq(User::getStatus, 1));
|
||||||
|
|
||||||
|
if (fakes.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机选15-20个作为今日活跃
|
||||||
|
int activeCount = 15 + RANDOM.nextInt(6);
|
||||||
|
activeCount = Math.min(activeCount, fakes.size());
|
||||||
|
|
||||||
|
Collections.shuffle(fakes);
|
||||||
|
List<String> activeUserIds = new ArrayList<>();
|
||||||
|
for (int i = 0; i < activeCount; i++) {
|
||||||
|
activeUserIds.add(String.valueOf(fakes.get(i).getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存入Redis
|
||||||
|
String today = java.time.LocalDate.now().toString();
|
||||||
|
redisTemplate.opsForSet().add(FAKE_ACTIVE_KEY + today, activeUserIds.toArray(new String[0]));
|
||||||
|
|
||||||
|
log.info("今日活跃假人已更新: {}个", activeCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否是今日活跃假人
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isTodayActiveFake(Long userId) {
|
||||||
|
String today = java.time.LocalDate.now().toString();
|
||||||
|
Boolean isMember = redisTemplate.opsForSet().isMember(FAKE_ACTIVE_KEY + today, String.valueOf(userId));
|
||||||
|
return Boolean.TRUE.equals(isMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 假人下注
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void fakeBet(Long groupId) {
|
||||||
|
// 获取今日活跃假人
|
||||||
|
String today = java.time.LocalDate.now().toString();
|
||||||
|
Set<String> activeFakes = redisTemplate.opsForSet().members(FAKE_ACTIVE_KEY + today);
|
||||||
|
if (activeFakes == null || activeFakes.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机选一个假人
|
||||||
|
List<String> fakeList = new ArrayList<>(activeFakes);
|
||||||
|
String fakeIdStr = fakeList.get(RANDOM.nextInt(fakeList.size()));
|
||||||
|
Long fakeId = Long.parseLong(fakeIdStr);
|
||||||
|
|
||||||
|
// 检查是否有进行中的局
|
||||||
|
GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
|
||||||
|
.eq(GameRound::getGroupId, groupId)
|
||||||
|
.eq(GameRound::getStatus, 0));
|
||||||
|
|
||||||
|
if (round == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机玩法和金额
|
||||||
|
String[] playTypes = {"单", "双", "大", "小"};
|
||||||
|
String playType = playTypes[RANDOM.nextInt(playTypes.length)];
|
||||||
|
int amount = (1 + RANDOM.nextInt(10)) * 10; // 10,20,30...100
|
||||||
|
|
||||||
|
try {
|
||||||
|
betService.placeBet(fakeId, groupId, "fake_" + UUID.randomUUID(), playType, amount);
|
||||||
|
log.debug("假人{}下注: {} {}", fakeId, playType, amount);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("假人下注失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取假人列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<User> getFakeUsers() {
|
||||||
|
return userMapper.selectList(new LambdaQueryWrapper<User>()
|
||||||
|
.eq(User::getIsFake, 1)
|
||||||
|
.eq(User::getStatus, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
package com.tailbet.service.impl;
|
||||||
|
|
||||||
|
import com.tailbet.entity.GameRound;
|
||||||
|
import com.tailbet.entity.DrawRecord;
|
||||||
|
import com.tailbet.entity.BetOrder;
|
||||||
|
import com.tailbet.entity.Group;
|
||||||
|
import com.tailbet.entity.User;
|
||||||
|
import com.tailbet.mapper.GameRoundMapper;
|
||||||
|
import com.tailbet.mapper.DrawRecordMapper;
|
||||||
|
import com.tailbet.mapper.BetOrderMapper;
|
||||||
|
import com.tailbet.mapper.GroupMapper;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.service.IGameService;
|
||||||
|
import com.tailbet.service.IPointsService;
|
||||||
|
import com.tailbet.service.IAuditService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
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.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏Service实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class GameServiceImpl implements IGameService {
|
||||||
|
|
||||||
|
private final GameRoundMapper gameRoundMapper;
|
||||||
|
private final DrawRecordMapper drawRecordMapper;
|
||||||
|
private final BetOrderMapper betOrderMapper;
|
||||||
|
private final GroupMapper groupMapper;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final IPointsService pointsService;
|
||||||
|
private final IAuditService auditService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群当前进行中的局
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public GameRound getOngoingRound(Long groupId) {
|
||||||
|
return gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
|
||||||
|
.eq(GameRound::getGroupId, groupId)
|
||||||
|
.eq(GameRound::getStatus, STATUS_ONGOING));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成期号
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String generateRoundNo(Long groupId) {
|
||||||
|
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||||
|
// 查询当日该群最后一期
|
||||||
|
LambdaQueryWrapper<GameRound> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(GameRound::getGroupId, groupId)
|
||||||
|
.likeRight(GameRound::getRoundNo, date)
|
||||||
|
.orderByDesc(GameRound::getCreateTime)
|
||||||
|
.last("LIMIT 1");
|
||||||
|
|
||||||
|
GameRound lastRound = gameRoundMapper.selectOne(wrapper);
|
||||||
|
int seq = 1;
|
||||||
|
if (lastRound != null) {
|
||||||
|
String lastNo = lastRound.getRoundNo();
|
||||||
|
String seqStr = lastNo.substring(lastNo.lastIndexOf("-") + 1);
|
||||||
|
seq = Integer.parseInt(seqStr) + 1;
|
||||||
|
}
|
||||||
|
return date + "-" + String.format("%02d", seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏(群主操作)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public GameRound startGame(Long groupId, Long userId) {
|
||||||
|
// 校验群主权限
|
||||||
|
Group group = groupMapper.selectById(groupId);
|
||||||
|
if (group == null || !group.getOwnerUserId().equals(userId)) {
|
||||||
|
throw new RuntimeException("只有群主可以开始游戏");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有进行中的局
|
||||||
|
if (getOngoingRound(groupId) != null) {
|
||||||
|
throw new RuntimeException("当前已有进行中的局");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新局
|
||||||
|
GameRound round = new GameRound();
|
||||||
|
round.setGroupId(groupId);
|
||||||
|
round.setRoundNo(generateRoundNo(groupId));
|
||||||
|
round.setStatus(STATUS_ONGOING);
|
||||||
|
round.setStartTime(LocalDateTime.now());
|
||||||
|
round.setDigitStrategy("random");
|
||||||
|
round.setAutoNext(1);
|
||||||
|
gameRoundMapper.insert(round);
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(groupId, round.getId(), userId, "GAME_START",
|
||||||
|
String.format("{\"roundNo\":\"%s\"}", round.getRoundNo()));
|
||||||
|
|
||||||
|
return round;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束接注
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public GameRound endBet(Long groupId, Long userId) {
|
||||||
|
GameRound round = getOngoingRound(groupId);
|
||||||
|
if (round == null) {
|
||||||
|
throw new RuntimeException("没有进行中的局");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验群主权限
|
||||||
|
Group group = groupMapper.selectById(groupId);
|
||||||
|
if (!group.getOwnerUserId().equals(userId)) {
|
||||||
|
throw new RuntimeException("只有群主可以结束接注");
|
||||||
|
}
|
||||||
|
|
||||||
|
round.setStatus(STATUS_ENDED);
|
||||||
|
round.setEndBetTime(LocalDateTime.now());
|
||||||
|
gameRoundMapper.updateById(round);
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(groupId, round.getId(), userId, "GAME_END_BET", "{}");
|
||||||
|
|
||||||
|
return round;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止自动连开
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void stopAutoNext(Long groupId, Long userId) {
|
||||||
|
GameRound round = getOngoingRound(groupId);
|
||||||
|
if (round == null) {
|
||||||
|
throw new RuntimeException("没有进行中的局");
|
||||||
|
}
|
||||||
|
|
||||||
|
Group group = groupMapper.selectById(groupId);
|
||||||
|
if (!group.getOwnerUserId().equals(userId)) {
|
||||||
|
throw new RuntimeException("只有群主可以停止自动连开");
|
||||||
|
}
|
||||||
|
|
||||||
|
round.setAutoNext(0);
|
||||||
|
gameRoundMapper.updateById(round);
|
||||||
|
|
||||||
|
auditService.log(groupId, round.getId(), userId, "STOP_AUTO_NEXT", "{}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置目标尾数
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void setTargetDigit(Long groupId, Integer digit, Long userId) {
|
||||||
|
GameRound round = getOngoingRound(groupId);
|
||||||
|
if (round == null) {
|
||||||
|
throw new RuntimeException("没有进行中的局");
|
||||||
|
}
|
||||||
|
|
||||||
|
round.setTargetDigit(digit);
|
||||||
|
round.setDigitStrategy("specified");
|
||||||
|
gameRoundMapper.updateById(round);
|
||||||
|
|
||||||
|
auditService.log(groupId, round.getId(), userId, "SET_DIGIT",
|
||||||
|
String.format("{\"digit\":%d}", digit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尾数试算
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<IGameService.DigitTrial> trialDigit(Long groupId, Long roundId) {
|
||||||
|
GameRound round;
|
||||||
|
if (roundId != null) {
|
||||||
|
round = gameRoundMapper.selectById(roundId);
|
||||||
|
} else {
|
||||||
|
round = getOngoingRound(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (round == null) {
|
||||||
|
throw new RuntimeException("没有进行中的局");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本局所有注单
|
||||||
|
List<BetOrder> bets = betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>()
|
||||||
|
.eq(BetOrder::getRoundId, round.getId())
|
||||||
|
.eq(BetOrder::getIsFake, 0));
|
||||||
|
|
||||||
|
// 计算0-9各尾数的庄家利润
|
||||||
|
java.util.List<IGameService.DigitTrial> result = new java.util.ArrayList<>();
|
||||||
|
for (int d = 0; d <= 9; d++) {
|
||||||
|
IGameService.DigitTrial trial = new IGameService.DigitTrial();
|
||||||
|
trial.setDigit(d);
|
||||||
|
trial.setPlayWin(getPlayWinDesc(d));
|
||||||
|
|
||||||
|
int totalBet = 0;
|
||||||
|
long totalAward = 0;
|
||||||
|
|
||||||
|
for (BetOrder bet : bets) {
|
||||||
|
totalBet += bet.getBetAmount();
|
||||||
|
if (isWin(bet, d)) {
|
||||||
|
totalAward += bet.getBetAmount() * (1 + bet.getOdds().doubleValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trial.setTotalBet(totalBet);
|
||||||
|
trial.setTotalAward(totalAward);
|
||||||
|
trial.setProfit(totalBet - totalAward);
|
||||||
|
result.add(trial);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判定是否中奖
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isWin(BetOrder bet, int digit) {
|
||||||
|
String playType = bet.getPlayType();
|
||||||
|
if ("单".equals(playType)) {
|
||||||
|
return digit % 2 == 1; // 奇数为单
|
||||||
|
} else if ("双".equals(playType)) {
|
||||||
|
return digit % 2 == 0; // 偶数为双(含0)
|
||||||
|
} else if ("大".equals(playType)) {
|
||||||
|
return digit >= 5;
|
||||||
|
} else if ("小".equals(playType)) {
|
||||||
|
return digit < 5;
|
||||||
|
} else if (playType.matches("[0-9]")) {
|
||||||
|
return Integer.parseInt(playType) == digit;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取中奖玩法描述
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getPlayWinDesc(int digit) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(digit % 2 == 1 ? "单" : "双");
|
||||||
|
sb.append("/");
|
||||||
|
sb.append(digit >= 5 ? "大" : "小");
|
||||||
|
sb.append("/");
|
||||||
|
sb.append("数字").append(digit);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开奖结算
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void settle(Long roundId, int digit) {
|
||||||
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
|
if (round == null) {
|
||||||
|
throw new RuntimeException("局不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本局所有注单
|
||||||
|
List<BetOrder> bets = betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>()
|
||||||
|
.eq(BetOrder::getRoundId, roundId)
|
||||||
|
.eq(BetOrder::getStatus, 0)); // 待结算
|
||||||
|
|
||||||
|
long totalBet = 0;
|
||||||
|
long totalAward = 0;
|
||||||
|
|
||||||
|
for (BetOrder bet : bets) {
|
||||||
|
totalBet += bet.getBetAmount();
|
||||||
|
|
||||||
|
if (isWin(bet, digit)) {
|
||||||
|
// 中奖,派奖
|
||||||
|
long winAmount = (long) (bet.getBetAmount() * (1 + bet.getOdds().doubleValue()));
|
||||||
|
bet.setStatus(1); // 已结算
|
||||||
|
bet.setWinAmount(winAmount);
|
||||||
|
betOrderMapper.updateById(bet);
|
||||||
|
|
||||||
|
// 给用户加积分
|
||||||
|
pointsService.addPoints(bet.getUserId(), winAmount, IPointsService.TYPE_AWARD,
|
||||||
|
String.valueOf(bet.getId()), null, "游戏中奖");
|
||||||
|
|
||||||
|
totalAward += winAmount;
|
||||||
|
} else {
|
||||||
|
// 未中奖
|
||||||
|
bet.setStatus(2); // 未中奖
|
||||||
|
betOrderMapper.updateById(bet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录开奖结果
|
||||||
|
DrawRecord record = new DrawRecord();
|
||||||
|
record.setGroupId(round.getGroupId());
|
||||||
|
record.setRoundNo(round.getRoundNo());
|
||||||
|
record.setRoundId(roundId);
|
||||||
|
record.setDigit(digit);
|
||||||
|
record.setPlayWins(getPlayWinDesc(digit));
|
||||||
|
record.setTotalBet((int) totalBet);
|
||||||
|
record.setTotalAward(totalAward);
|
||||||
|
record.setProfit(totalBet - totalAward);
|
||||||
|
record.setCreateTime(LocalDateTime.now());
|
||||||
|
drawRecordMapper.insert(record);
|
||||||
|
|
||||||
|
// 更新局状态
|
||||||
|
round.setFinishTime(LocalDateTime.now());
|
||||||
|
round.setStatus(STATUS_ENDED);
|
||||||
|
gameRoundMapper.updateById(round);
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(round.getGroupId(), roundId, null, "SETTLE",
|
||||||
|
String.format("{\"digit\":%d,\"totalBet\":%d,\"totalAward\":%d,\"profit\":%d}",
|
||||||
|
digit, totalBet, totalAward, totalBet - totalAward));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取开奖历史
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<DrawRecord> getHistory(Long groupId, Integer pageNum, Integer pageSize) {
|
||||||
|
Page<DrawRecord> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<DrawRecord> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(DrawRecord::getGroupId, groupId)
|
||||||
|
.orderByDesc(DrawRecord::getCreateTime);
|
||||||
|
return drawRecordMapper.selectPage(page, wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package com.tailbet.service.impl;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.Group;
|
||||||
|
import com.tailbet.model.entity.GroupMember;
|
||||||
|
import com.tailbet.mapper.GroupMapper;
|
||||||
|
import com.tailbet.mapper.GroupMemberMapper;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.service.IGroupService;
|
||||||
|
import com.tailbet.service.IAuditService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群Service实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class GroupServiceImpl implements IGroupService {
|
||||||
|
|
||||||
|
private final GroupMapper groupMapper;
|
||||||
|
private final GroupMemberMapper groupMemberMapper;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final IAuditService auditService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群信息
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Group getById(Long groupId) {
|
||||||
|
return groupMapper.selectById(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的群列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<Group> getUserGroups(Long userId) {
|
||||||
|
List<GroupMember> members = groupMemberMapper.selectList(new LambdaQueryWrapper<GroupMember>()
|
||||||
|
.eq(GroupMember::getUserId, userId));
|
||||||
|
|
||||||
|
if (members.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> groupIds = members.stream()
|
||||||
|
.map(GroupMember::getGroupId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return groupMapper.selectList(new LambdaQueryWrapper<Group>()
|
||||||
|
.in(Group::getId, groupIds)
|
||||||
|
.eq(Group::getStatus, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉用户入群
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void pullUser(Long groupId, Long userId, Long operatorId) {
|
||||||
|
// 检查是否已在群中
|
||||||
|
GroupMember existing = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
|
||||||
|
.eq(GroupMember::getGroupId, groupId)
|
||||||
|
.eq(GroupMember::getUserId, userId));
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
throw new RuntimeException("用户已在群中");
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupMember member = new GroupMember();
|
||||||
|
member.setGroupId(groupId);
|
||||||
|
member.setUserId(userId);
|
||||||
|
member.setRole(ROLE_NORMAL);
|
||||||
|
member.setJoinTime(LocalDateTime.now());
|
||||||
|
member.setCreateTime(LocalDateTime.now());
|
||||||
|
member.setUpdateTime(LocalDateTime.now());
|
||||||
|
groupMemberMapper.insert(member);
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(groupId, null, operatorId, "PULL_USER",
|
||||||
|
String.format("{\"userId\":%d}", userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 踢出用户
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void kickUser(Long groupId, Long targetUserId, Long operatorId) {
|
||||||
|
Group group = groupMapper.selectById(groupId);
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
GroupMember operator = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
|
||||||
|
.eq(GroupMember::getGroupId, groupId)
|
||||||
|
.eq(GroupMember::getUserId, operatorId));
|
||||||
|
|
||||||
|
if (operator == null) {
|
||||||
|
throw new RuntimeException("你不是群成员");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有群主和管理员可以踢人
|
||||||
|
if (operator.getRole() != ROLE_OWNER && operator.getRole() != ROLE_ADMIN) {
|
||||||
|
throw new RuntimeException("只有群主和管理员可以踢人");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不能踢群主
|
||||||
|
if (group.getOwnerUserId().equals(targetUserId)) {
|
||||||
|
throw new RuntimeException("不能踢出群主");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除成员
|
||||||
|
groupMemberMapper.delete(new LambdaQueryWrapper<GroupMember>()
|
||||||
|
.eq(GroupMember::getGroupId, groupId)
|
||||||
|
.eq(GroupMember::getUserId, targetUserId));
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(groupId, null, operatorId, "KICK_USER",
|
||||||
|
String.format("{\"targetUserId\":%d}", targetUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置管理员
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void setAdmin(Long groupId, Long targetUserId, Long operatorId) {
|
||||||
|
Group group = groupMapper.selectById(groupId);
|
||||||
|
|
||||||
|
// 只有群主可以设置管理员
|
||||||
|
if (!group.getOwnerUserId().equals(operatorId)) {
|
||||||
|
throw new RuntimeException("只有群主可以设置管理员");
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupMember member = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
|
||||||
|
.eq(GroupMember::getGroupId, groupId)
|
||||||
|
.eq(GroupMember::getUserId, targetUserId));
|
||||||
|
|
||||||
|
if (member == null) {
|
||||||
|
throw new RuntimeException("目标用户不是群成员");
|
||||||
|
}
|
||||||
|
|
||||||
|
member.setRole(ROLE_ADMIN);
|
||||||
|
member.setUpdateTime(LocalDateTime.now());
|
||||||
|
groupMemberMapper.updateById(member);
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(groupId, null, operatorId, "SET_ADMIN",
|
||||||
|
String.format("{\"targetUserId\":%d}", targetUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消管理员
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void removeAdmin(Long groupId, Long targetUserId, Long operatorId) {
|
||||||
|
Group group = groupMapper.selectById(groupId);
|
||||||
|
|
||||||
|
if (!group.getOwnerUserId().equals(operatorId)) {
|
||||||
|
throw new RuntimeException("只有群主可以取消管理员");
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupMember member = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
|
||||||
|
.eq(GroupMember::getGroupId, groupId)
|
||||||
|
.eq(GroupMember::getUserId, targetUserId));
|
||||||
|
|
||||||
|
if (member == null) {
|
||||||
|
throw new RuntimeException("目标用户不是群成员");
|
||||||
|
}
|
||||||
|
|
||||||
|
member.setRole(ROLE_NORMAL);
|
||||||
|
member.setUpdateTime(LocalDateTime.now());
|
||||||
|
groupMemberMapper.updateById(member);
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(groupId, null, operatorId, "REMOVE_ADMIN",
|
||||||
|
String.format("{\"targetUserId\":%d}", targetUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群成员
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<GroupMember> getMembers(Long groupId) {
|
||||||
|
return groupMemberMapper.selectList(new LambdaQueryWrapper<GroupMember>()
|
||||||
|
.eq(GroupMember::getGroupId, groupId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否是群主
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isOwner(Long groupId, Long userId) {
|
||||||
|
Group group = groupMapper.selectById(groupId);
|
||||||
|
return group != null && group.getOwnerUserId().equals(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否是管理员
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isAdmin(Long groupId, Long userId) {
|
||||||
|
GroupMember member = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
|
||||||
|
.eq(GroupMember::getGroupId, groupId)
|
||||||
|
.eq(GroupMember::getUserId, userId));
|
||||||
|
return member != null && (member.getRole() == ROLE_ADMIN || member.getRole() == ROLE_OWNER);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package com.tailbet.service.impl;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.PointsFlow;
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.tailbet.mapper.PointsFlowMapper;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.service.IPointsService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分Service实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class PointsServiceImpl implements IPointsService {
|
||||||
|
|
||||||
|
private final PointsFlowMapper pointsFlowMapper;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加积分
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void addPoints(Long userId, Long amount, String type, String bizNo, Long operatorId, String remark) {
|
||||||
|
User user = userMapper.selectById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new RuntimeException("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Long before = user.getPoints();
|
||||||
|
Long after = before + amount;
|
||||||
|
|
||||||
|
user.setPoints(after);
|
||||||
|
userMapper.updateById(user);
|
||||||
|
|
||||||
|
// 记录流水
|
||||||
|
PointsFlow flow = new PointsFlow();
|
||||||
|
flow.setUserId(userId);
|
||||||
|
flow.setType(type);
|
||||||
|
flow.setAmount(amount);
|
||||||
|
flow.setBalanceBefore(before);
|
||||||
|
flow.setBalanceAfter(after);
|
||||||
|
flow.setBizNo(bizNo);
|
||||||
|
flow.setOperatorId(operatorId);
|
||||||
|
flow.setRemark(remark);
|
||||||
|
pointsFlowMapper.insert(flow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扣除积分
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public boolean subPoints(Long userId, Long amount, String type, String bizNo, Long operatorId, String remark) {
|
||||||
|
User user = userMapper.selectById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new RuntimeException("用户不存在");
|
||||||
|
}
|
||||||
|
if (user.getPoints() < amount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long before = user.getPoints();
|
||||||
|
Long after = before - amount;
|
||||||
|
|
||||||
|
user.setPoints(after);
|
||||||
|
userMapper.updateById(user);
|
||||||
|
|
||||||
|
// 记录流水
|
||||||
|
PointsFlow flow = new PointsFlow();
|
||||||
|
flow.setUserId(userId);
|
||||||
|
flow.setType(type);
|
||||||
|
flow.setAmount(-amount);
|
||||||
|
flow.setBalanceBefore(before);
|
||||||
|
flow.setBalanceAfter(after);
|
||||||
|
flow.setBizNo(bizNo);
|
||||||
|
flow.setOperatorId(operatorId);
|
||||||
|
flow.setRemark(remark);
|
||||||
|
pointsFlowMapper.insert(flow);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户积分
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Long getPoints(Long userId) {
|
||||||
|
User user = userMapper.selectById(userId);
|
||||||
|
return user != null ? user.getPoints() : 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询积分流水记录
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<PointsFlow> getFlowPage(Long userId, Integer pageNum, Integer pageSize) {
|
||||||
|
Page<PointsFlow> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<PointsFlow> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PointsFlow::getUserId, userId)
|
||||||
|
.orderByDesc(PointsFlow::getCreateTime);
|
||||||
|
return pointsFlowMapper.selectPage(page, wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package com.tailbet.service.impl;
|
||||||
|
|
||||||
|
import com.tailbet.entity.User;
|
||||||
|
import com.tailbet.entity.DigitWhite;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.mapper.DigitWhiteMapper;
|
||||||
|
import com.tailbet.openim.OpenIMClient;
|
||||||
|
import com.tailbet.service.IPushService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class PushServiceImpl implements IPushService {
|
||||||
|
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final DigitWhiteMapper digitWhiteMapper;
|
||||||
|
private final OpenIMClient openIMClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送尾数试算完成通知给白名单用户
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void pushDigitTrialComplete(Long groupId, Long roundId) {
|
||||||
|
List<DigitWhite> whites = digitWhiteMapper.selectList(new LambdaQueryWrapper<DigitWhite>());
|
||||||
|
for (DigitWhite white : whites) {
|
||||||
|
User user = userMapper.selectById(white.getUserId());
|
||||||
|
if (user != null) {
|
||||||
|
PushService.PushMessage message = new PushService.PushMessage();
|
||||||
|
message.setType(PUSH_TYPE_DIGIT_TRIAL);
|
||||||
|
message.setTitle("尾数试算完成");
|
||||||
|
message.setContent("可前往修改尾数");
|
||||||
|
message.setUserId(user.getId());
|
||||||
|
message.setGroupId(groupId);
|
||||||
|
message.setRoundId(roundId);
|
||||||
|
|
||||||
|
sendPush(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送游戏开始通知
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void pushGameStart(Long groupId, String roundNo) {
|
||||||
|
PushService.PushMessage message = new PushService.PushMessage();
|
||||||
|
message.setType(PUSH_TYPE_GAME_START);
|
||||||
|
message.setTitle("游戏开始");
|
||||||
|
message.setContent(roundNo + "期已开始下注");
|
||||||
|
message.setGroupId(groupId);
|
||||||
|
message.setRoundNo(roundNo);
|
||||||
|
|
||||||
|
// 推送给所有群成员
|
||||||
|
broadcastToGroup(groupId, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送开奖结果通知
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void pushGameResult(Long groupId, String roundNo, int digit, String playWins) {
|
||||||
|
PushService.PushMessage message = new PushService.PushMessage();
|
||||||
|
message.setType(PUSH_TYPE_RESULT);
|
||||||
|
message.setTitle("开奖结果");
|
||||||
|
message.setContent(roundNo + "期开奖\n尾数: " + digit + "\n中奖玩法: " + playWins);
|
||||||
|
message.setGroupId(groupId);
|
||||||
|
message.setRoundNo(roundNo);
|
||||||
|
message.setDigit(digit);
|
||||||
|
|
||||||
|
// 广播给所有群成员
|
||||||
|
broadcastToGroup(groupId, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送单用户推送
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void sendPush(PushService.PushMessage message) {
|
||||||
|
try {
|
||||||
|
User user = userMapper.selectById(message.getUserId());
|
||||||
|
if (user == null) {
|
||||||
|
log.warn("推送用户不存在: userId={}", message.getUserId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 根据实际推送通道实现
|
||||||
|
// 1. 如果用户在线,通过OpenIM发送即时消息
|
||||||
|
// 2. 如果用户离线,通过FCM/APNs发送离线推送
|
||||||
|
|
||||||
|
// 通过OpenIM单聊发送
|
||||||
|
openIMClient.sendUserMessage(String.valueOf(user.getId()),
|
||||||
|
message.getTitle() + " - " + message.getContent());
|
||||||
|
|
||||||
|
log.info("推送发送成功: userId={}, type={}", message.getUserId(), message.getType());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("推送发送失败: userId={}, type={}, error={}",
|
||||||
|
message.getUserId(), message.getType(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播推送消息给群成员
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void broadcastToGroup(Long groupId, PushService.PushMessage message) {
|
||||||
|
try {
|
||||||
|
// 通过OpenIM群消息发送
|
||||||
|
openIMClient.sendGroupMessage(String.valueOf(groupId),
|
||||||
|
message.getContent());
|
||||||
|
log.info("群广播发送成功: groupId={}, type={}", groupId, message.getType());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("群广播发送失败: groupId={}, type={}, error={}",
|
||||||
|
groupId, message.getType(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
package com.tailbet.service.impl;
|
||||||
|
|
||||||
|
import com.tailbet.model.entity.RedPacket;
|
||||||
|
import com.tailbet.model.entity.RedPacketRecv;
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.tailbet.mapper.RedPacketMapper;
|
||||||
|
import com.tailbet.mapper.RedPacketRecvMapper;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.mapper.GameRoundMapper;
|
||||||
|
import com.tailbet.service.IRedPacketService;
|
||||||
|
import com.tailbet.service.IPointsService;
|
||||||
|
import com.tailbet.service.IAuditService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
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.math.RoundingMode;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包Service实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class RedPacketServiceImpl implements IRedPacketService {
|
||||||
|
|
||||||
|
private final RedPacketMapper redPacketMapper;
|
||||||
|
private final RedPacketRecvMapper redPacketRecvMapper;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final GameRoundMapper gameRoundMapper;
|
||||||
|
private final IPointsService pointsService;
|
||||||
|
private final IAuditService auditService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发红包
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public RedPacket sendRp(Long userId, Long groupId, BigDecimal totalAmount,
|
||||||
|
Integer totalCount, Long roundId) {
|
||||||
|
User user = userMapper.selectById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new RuntimeException("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扣积分
|
||||||
|
boolean success = pointsService.subPoints(userId, totalAmount.longValue(),
|
||||||
|
IPointsService.TYPE_RP_SEND, null, null, "发送红包");
|
||||||
|
if (!success) {
|
||||||
|
throw new RuntimeException("余额不足");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建红包
|
||||||
|
RedPacket rp = new RedPacket();
|
||||||
|
rp.setUserId(userId);
|
||||||
|
rp.setGroupId(groupId);
|
||||||
|
rp.setRoundId(roundId);
|
||||||
|
rp.setTotalAmount(totalAmount);
|
||||||
|
rp.setTotalCount(totalCount);
|
||||||
|
rp.setRemainAmount(totalAmount);
|
||||||
|
rp.setRemainCount(totalCount);
|
||||||
|
rp.setStatus(STATUS_ONGOING);
|
||||||
|
rp.setCreateTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
// 判断红包类型
|
||||||
|
if (roundId != null) {
|
||||||
|
rp.setType(TYPE_GAME);
|
||||||
|
} else {
|
||||||
|
rp.setType(TYPE_RANDOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
redPacketMapper.insert(rp);
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(groupId, roundId, userId, "RP_SEND",
|
||||||
|
String.format("{\"rpId\":%d,\"amount\":%s,\"count\":%d}",
|
||||||
|
rp.getId(), totalAmount, totalCount));
|
||||||
|
|
||||||
|
return rp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领红包
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public RedPacketRecv receiveRp(Long rpId, Long userId) {
|
||||||
|
RedPacket rp = redPacketMapper.selectById(rpId);
|
||||||
|
if (rp == null) {
|
||||||
|
throw new RuntimeException("红包不存在");
|
||||||
|
}
|
||||||
|
if (rp.getStatus() != STATUS_ONGOING) {
|
||||||
|
throw new RuntimeException("红包已领完");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已领取
|
||||||
|
RedPacketRecv existing = redPacketRecvMapper.selectOne(new LambdaQueryWrapper<RedPacketRecv>()
|
||||||
|
.eq(RedPacketRecv::getRpId, rpId)
|
||||||
|
.eq(RedPacketRecv::getUserId, userId));
|
||||||
|
if (existing != null) {
|
||||||
|
throw new RuntimeException("已领取过该红包");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算领取金额
|
||||||
|
BigDecimal amount = calculateReceiveAmount(rp);
|
||||||
|
|
||||||
|
// 更新红包
|
||||||
|
rp.setRemainAmount(rp.getRemainAmount().subtract(amount));
|
||||||
|
rp.setRemainCount(rp.getRemainCount() - 1);
|
||||||
|
if (rp.getRemainCount() <= 0) {
|
||||||
|
rp.setStatus(STATUS_FINISHED);
|
||||||
|
}
|
||||||
|
redPacketMapper.updateById(rp);
|
||||||
|
|
||||||
|
// 加积分
|
||||||
|
pointsService.addPoints(userId, amount.longValue(),
|
||||||
|
IPointsService.TYPE_RP_RECV, String.valueOf(rpId), null, "领取红包");
|
||||||
|
|
||||||
|
// 记录领取
|
||||||
|
RedPacketRecv recv = new RedPacketRecv();
|
||||||
|
recv.setRpId(rpId);
|
||||||
|
recv.setUserId(userId);
|
||||||
|
recv.setAmount(amount);
|
||||||
|
recv.setCreateTime(LocalDateTime.now());
|
||||||
|
redPacketRecvMapper.insert(recv);
|
||||||
|
|
||||||
|
// 审计日志
|
||||||
|
auditService.log(rp.getGroupId(), rp.getRoundId(), userId, "RP_RECV",
|
||||||
|
String.format("{\"rpId\":%d,\"amount\":%s}", rpId, amount));
|
||||||
|
|
||||||
|
return recv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算领取金额
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public BigDecimal calculateReceiveAmount(RedPacket rp) {
|
||||||
|
if (rp.getRemainCount() <= 0) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是游戏红包且有目标尾数,则需要控制手气王
|
||||||
|
// 简化实现:随机分配
|
||||||
|
Random random = new Random();
|
||||||
|
if (rp.getRemainCount() == 1) {
|
||||||
|
// 最后一个,拿剩余全部
|
||||||
|
return rp.getRemainAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机金额,最小0.01
|
||||||
|
double min = 0.01;
|
||||||
|
double max = rp.getRemainAmount().divide(BigDecimal.valueOf(rp.getRemainCount()), 2, RoundingMode.HALF_UP).doubleValue() * 2;
|
||||||
|
double amount = min + (max - min) * random.nextDouble();
|
||||||
|
return BigDecimal.valueOf(amount).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取红包详情
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public RedPacket getById(Long rpId) {
|
||||||
|
return redPacketMapper.selectById(rpId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取红包领取列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<RedPacketRecv> getReceives(Long rpId) {
|
||||||
|
return redPacketRecvMapper.selectList(new LambdaQueryWrapper<RedPacketRecv>()
|
||||||
|
.eq(RedPacketRecv::getRpId, rpId)
|
||||||
|
.orderByAsc(RedPacketRecv::getCreateTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询红包记录
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<RedPacket> getRecords(Long userId, Integer pageNum, Integer pageSize) {
|
||||||
|
Page<RedPacket> page = new Page<>(pageNum, pageSize);
|
||||||
|
LambdaQueryWrapper<RedPacket> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(RedPacket::getUserId, userId)
|
||||||
|
.orderByDesc(RedPacket::getCreateTime);
|
||||||
|
return redPacketMapper.selectPage(page, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找进行中的游戏红包
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public RedPacket getOngoingGameRp(Long groupId) {
|
||||||
|
return redPacketMapper.selectOne(new LambdaQueryWrapper<RedPacket>()
|
||||||
|
.eq(RedPacket::getGroupId, groupId)
|
||||||
|
.eq(RedPacket::getType, TYPE_GAME)
|
||||||
|
.eq(RedPacket::getStatus, STATUS_ONGOING)
|
||||||
|
.isNotNull(RedPacket::getRoundId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package com.tailbet.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.model.dto.UpdateUserDTO;
|
||||||
|
import com.tailbet.service.IUserService;
|
||||||
|
import com.tailbet.util.JwtUtil;
|
||||||
|
import com.tailbet.util.PasswordUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户Service实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class UserServiceImpl implements IUserService {
|
||||||
|
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final PasswordUtil passwordUtil;
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getById(Long id) {
|
||||||
|
return userMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名查询用户
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public User getByUsername(String username) {
|
||||||
|
return userMapper.selectOne(new LambdaQueryWrapper<User>()
|
||||||
|
.eq(User::getUsername, username));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册用户
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public User register(String username, String password) {
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
if (getByUsername(username) != null) {
|
||||||
|
throw new RuntimeException("用户名已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(passwordUtil.encode(password));
|
||||||
|
user.setNickname(username);
|
||||||
|
user.setPoints(0L);
|
||||||
|
user.setStatus(1);
|
||||||
|
userMapper.insert(user);
|
||||||
|
|
||||||
|
// 分配运营(按最少绑定+优先在线算法)
|
||||||
|
assignOps(user);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String login(String username, String password) {
|
||||||
|
User user = getByUsername(username);
|
||||||
|
if (user == null) {
|
||||||
|
throw new RuntimeException("用户不存在");
|
||||||
|
}
|
||||||
|
if (user.getStatus() == 0) {
|
||||||
|
throw new RuntimeException("账号已被封禁");
|
||||||
|
}
|
||||||
|
if (!passwordUtil.matches(password, user.getPassword())) {
|
||||||
|
throw new RuntimeException("密码错误");
|
||||||
|
}
|
||||||
|
return jwtUtil.generateToken(user.getId(), user.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean updateUser(User user) {
|
||||||
|
return userMapper.updateById(user) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateUserInfo(Long userId, UpdateUserDTO dto) {
|
||||||
|
User user = userMapper.selectById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new RuntimeException("用户不存在");
|
||||||
|
}
|
||||||
|
if (dto.getNickname() != null && !dto.getNickname().isEmpty()) {
|
||||||
|
user.setNickname(dto.getNickname());
|
||||||
|
}
|
||||||
|
if (dto.getAvatarUrl() != null) {
|
||||||
|
user.setAvatarUrl(dto.getAvatarUrl());
|
||||||
|
}
|
||||||
|
if (dto.getPassword() != null && !dto.getPassword().isEmpty()) {
|
||||||
|
user.setPassword(passwordUtil.encode(dto.getPassword()));
|
||||||
|
}
|
||||||
|
userMapper.updateById(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public User getCurrentUser() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分配运营(粘性绑定:在线+最少绑定)
|
||||||
|
*/
|
||||||
|
private void assignOps(User user) {
|
||||||
|
// 查询所有运营账号
|
||||||
|
List<User> opsList = userMapper.selectList(new LambdaQueryWrapper<User>()
|
||||||
|
.eq(User::getIsOps, 1)
|
||||||
|
.eq(User::getStatus, 1));
|
||||||
|
|
||||||
|
if (opsList == null || opsList.isEmpty()) {
|
||||||
|
log.warn("没有可用的运营账号,用户{}将无法分配运营", user.getUsername());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找绑定用户最少的运营
|
||||||
|
User selectedOps = opsList.stream()
|
||||||
|
.min((a, b) -> {
|
||||||
|
Long countA = userMapper.selectCount(new LambdaQueryWrapper<User>()
|
||||||
|
.eq(User::getBoundOpsUserId, a.getId()));
|
||||||
|
Long countB = userMapper.selectCount(new LambdaQueryWrapper<User>()
|
||||||
|
.eq(User::getBoundOpsUserId, b.getId()));
|
||||||
|
return countA.compareTo(countB);
|
||||||
|
})
|
||||||
|
.orElse(opsList.get(0));
|
||||||
|
|
||||||
|
user.setBoundOpsUserId(selectedOps.getId());
|
||||||
|
userMapper.updateById(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.tailbet.util;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT工具类
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JwtUtil {
|
||||||
|
|
||||||
|
@Value("${jwt.secret}")
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
@Value("${jwt.expiration}")
|
||||||
|
private Long expiration;
|
||||||
|
|
||||||
|
private SecretKey getSigningKey() {
|
||||||
|
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||||
|
return Keys.hmacShaKeyFor(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成Token
|
||||||
|
*/
|
||||||
|
public String generateToken(Long userId, String username) {
|
||||||
|
Map<String, Object> claims = new HashMap<>();
|
||||||
|
claims.put("userId", userId);
|
||||||
|
claims.put("username", username);
|
||||||
|
return createToken(claims, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createToken(Map<String, Object> claims, String subject) {
|
||||||
|
Date now = new Date();
|
||||||
|
Date expirationDate = new Date(now.getTime() + expiration);
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.claims(claims)
|
||||||
|
.subject(subject)
|
||||||
|
.issuedAt(now)
|
||||||
|
.expiration(expirationDate)
|
||||||
|
.signWith(getSigningKey())
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析Token
|
||||||
|
*/
|
||||||
|
public Claims parseToken(String token) {
|
||||||
|
return Jwts.parser()
|
||||||
|
.verifyWith(getSigningKey())
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token)
|
||||||
|
.getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户ID
|
||||||
|
*/
|
||||||
|
public Long getUserId(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.get("userId", Long.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户名
|
||||||
|
*/
|
||||||
|
public String getUsername(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证Token是否过期
|
||||||
|
*/
|
||||||
|
public boolean isTokenExpired(String token) {
|
||||||
|
try {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.getExpiration().before(new Date());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.tailbet.util;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.crypto.digest.DigestUtil;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码工具类
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class PasswordUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BCrypt加密
|
||||||
|
*/
|
||||||
|
public String encode(String rawPassword) {
|
||||||
|
return DigestUtil.bcrypt(rawPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BCrypt校验
|
||||||
|
*/
|
||||||
|
public boolean matches(String rawPassword, String encodedPassword) {
|
||||||
|
if (StrUtil.isBlank(rawPassword) || StrUtil.isBlank(encodedPassword)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return DigestUtil.bcryptCheck(rawPassword, encodedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.tailbet.util;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis分布式锁注解
|
||||||
|
*/
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface RedisLock {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁的key,支持SpEL表达式
|
||||||
|
*/
|
||||||
|
String key();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁过期时间(秒)
|
||||||
|
*/
|
||||||
|
int expire() default 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否等待锁
|
||||||
|
*/
|
||||||
|
boolean waitLock() default false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待锁的最大时间(毫秒)
|
||||||
|
*/
|
||||||
|
long waitTime() default 3000;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.tailbet.util;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis分布式锁工具
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RedisLockUtil {
|
||||||
|
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
private static final String LOCK_PREFIX = "lock:";
|
||||||
|
|
||||||
|
private static final String UNLOCK_SCRIPT =
|
||||||
|
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
|
||||||
|
" return redis.call('del', KEYS[1]) " +
|
||||||
|
"else " +
|
||||||
|
" return 0 " +
|
||||||
|
"end";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加锁
|
||||||
|
*/
|
||||||
|
public boolean lock(String key, String value, long expireSeconds) {
|
||||||
|
String lockKey = LOCK_PREFIX + key;
|
||||||
|
Boolean result = redisTemplate.opsForValue()
|
||||||
|
.setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);
|
||||||
|
return Boolean.TRUE.equals(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解锁
|
||||||
|
*/
|
||||||
|
public boolean unlock(String key, String value) {
|
||||||
|
String lockKey = LOCK_PREFIX + key;
|
||||||
|
DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
|
||||||
|
Long result = redisTemplate.execute(script, Collections.singletonList(lockKey), value);
|
||||||
|
return result != null && result == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试获取锁
|
||||||
|
*/
|
||||||
|
public String tryLock(String key, long expireSeconds, long waitTimeMillis) {
|
||||||
|
String lockKey = LOCK_PREFIX + key;
|
||||||
|
String value = String.valueOf(System.currentTimeMillis());
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
while (System.currentTimeMillis() - startTime < waitTimeMillis) {
|
||||||
|
Boolean result = redisTemplate.opsForValue()
|
||||||
|
.setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);
|
||||||
|
if (Boolean.TRUE.equals(result)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
TimeUnit.MILLISECONDS.sleep(50);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放锁
|
||||||
|
*/
|
||||||
|
public void delete(String key) {
|
||||||
|
String lockKey = LOCK_PREFIX + key;
|
||||||
|
redisTemplate.delete(lockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: guess-game
|
||||||
|
profiles:
|
||||||
|
active: dev
|
||||||
|
datasource:
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
url: jdbc:mysql://localhost:3306/guess_game?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8
|
||||||
|
username: root
|
||||||
|
password: ${DB_PASSWORD:123456}
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: ${REDIS_HOST:localhost}
|
||||||
|
port: ${REDIS_PORT:6379}
|
||||||
|
password: ${REDIS_PASSWORD:123456}
|
||||||
|
database: ${REDIS_DB:0}
|
||||||
|
timeout: 5000ms
|
||||||
|
lettuce:
|
||||||
|
pool:
|
||||||
|
max-active: 8
|
||||||
|
max-wait: -1ms
|
||||||
|
max-idle: 8
|
||||||
|
min-idle: 0
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
mybatis-plus:
|
||||||
|
mapper-locations: classpath:mapper/**/*.xml
|
||||||
|
type-aliases-package: com.tailbet.entity
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
global-config:
|
||||||
|
db-config:
|
||||||
|
id-type: auto
|
||||||
|
logic-delete-field: deleted
|
||||||
|
logic-delete-value: 1
|
||||||
|
logic-not-delete-value: 0
|
||||||
|
|
||||||
|
openim:
|
||||||
|
api-url: ${OPENIM_API_URL:http://localhost:10010}
|
||||||
|
ws-url: ${OPENIM_WS_URL:ws://localhost:10010}
|
||||||
|
admin-user: ${OPENIM_ADMIN_USER:admin}
|
||||||
|
admin-token: ${OPENIM_ADMIN_TOKEN:}
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
secret: ${JWT_SECRET:your-256-bit-secret-key-for-jwt-token-generation}
|
||||||
|
expiration: 604800000
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.tailbet: debug
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
# 数据库初始化脚本
|
||||||
|
CREATE DATABASE IF NOT EXISTS guess_game DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE guess_game;
|
||||||
|
|
||||||
|
-- 用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE COMMENT '账号',
|
||||||
|
password VARCHAR(255) NOT NULL COMMENT '密码(BCrypt)',
|
||||||
|
nickname VARCHAR(50) DEFAULT '' COMMENT '昵称',
|
||||||
|
avatar_url VARCHAR(500) DEFAULT '' COMMENT '头像URL',
|
||||||
|
points BIGINT DEFAULT 0 COMMENT '积分余额',
|
||||||
|
is_ops TINYINT DEFAULT 0 COMMENT '运营标记:0否 1是',
|
||||||
|
is_bot TINYINT DEFAULT 0 COMMENT '机器人标记:0否 1是',
|
||||||
|
is_fake TINYINT DEFAULT 0 COMMENT '假人托标记:0否 1是',
|
||||||
|
is_white TINYINT DEFAULT 0 COMMENT '加白(尾数控制)标记',
|
||||||
|
bound_ops_user_id BIGINT DEFAULT 0 COMMENT '绑定的运营用户ID',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态:1正常 0封禁',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0否 1是',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_ops (is_ops),
|
||||||
|
INDEX idx_fake (is_fake),
|
||||||
|
INDEX idx_bound_ops (bound_ops_user_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) COMMENT '用户表';
|
||||||
|
|
||||||
|
-- 群表
|
||||||
|
CREATE TABLE IF NOT EXISTS `group` (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '群名称',
|
||||||
|
openim_group_id VARCHAR(100) NOT NULL COMMENT 'OpenIM群ID',
|
||||||
|
owner_user_id BIGINT NOT NULL COMMENT '群主用户ID',
|
||||||
|
round_interval INT DEFAULT 60 COMMENT '接注间隔(秒)',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_owner (owner_user_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) COMMENT '群表';
|
||||||
|
|
||||||
|
-- 群成员表
|
||||||
|
CREATE TABLE IF NOT EXISTS group_member (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
group_id BIGINT NOT NULL COMMENT '群ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||||
|
role TINYINT DEFAULT 0 COMMENT '角色:0普通 1管理员 2群主',
|
||||||
|
join_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
UNIQUE KEY uk_group_user (group_id, user_id),
|
||||||
|
INDEX idx_user (user_id)
|
||||||
|
) COMMENT '群成员表';
|
||||||
|
|
||||||
|
-- 积分流水表
|
||||||
|
CREATE TABLE IF NOT EXISTS points_flow (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||||
|
type VARCHAR(20) NOT NULL COMMENT '类型:ops_add/ops_sub/bet/award/rp_send/rp_recv',
|
||||||
|
amount BIGINT NOT NULL COMMENT '变动数值(正负)',
|
||||||
|
balance_before BIGINT NOT NULL COMMENT '变动前余额',
|
||||||
|
balance_after BIGINT NOT NULL COMMENT '变动后余额',
|
||||||
|
biz_no VARCHAR(100) COMMENT '业务单号',
|
||||||
|
operator_id BIGINT COMMENT '操作人ID',
|
||||||
|
remark VARCHAR(255) DEFAULT '' COMMENT '备注',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_user_time (user_id, create_time),
|
||||||
|
INDEX idx_biz_no (biz_no),
|
||||||
|
INDEX idx_type (type)
|
||||||
|
) COMMENT '积分流水表';
|
||||||
|
|
||||||
|
-- 游戏局表
|
||||||
|
CREATE TABLE IF NOT EXISTS game_round (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
group_id BIGINT NOT NULL COMMENT '群ID',
|
||||||
|
round_no VARCHAR(20) NOT NULL COMMENT '期号:yyyy-MM-dd-xx',
|
||||||
|
status TINYINT DEFAULT 0 COMMENT '状态:0进行中 1已结束',
|
||||||
|
start_time DATETIME COMMENT '开始时间',
|
||||||
|
end_bet_time DATETIME COMMENT '截止下注时间',
|
||||||
|
start_rp_time DATETIME COMMENT '开始发红包时间',
|
||||||
|
finish_time DATETIME COMMENT '结束时间',
|
||||||
|
target_digit INT COMMENT '目标尾数:0-9 null表示待定',
|
||||||
|
digit_strategy VARCHAR(20) DEFAULT 'random' COMMENT '尾数策略',
|
||||||
|
auto_next TINYINT DEFAULT 1 COMMENT '自动开启下一期:0否 1是',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_group_round (group_id, round_no),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_group_id (group_id)
|
||||||
|
) COMMENT '游戏局表';
|
||||||
|
|
||||||
|
-- 注单表
|
||||||
|
CREATE TABLE IF NOT EXISTS bet_order (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||||
|
group_id BIGINT NOT NULL COMMENT '群ID',
|
||||||
|
round_id BIGINT NOT NULL COMMENT '局ID',
|
||||||
|
client_msg_id VARCHAR(100) COMMENT '客户端消息ID(防重)',
|
||||||
|
play_type VARCHAR(10) NOT NULL COMMENT '玩法:单/双/大/小/数字',
|
||||||
|
bet_amount INT NOT NULL COMMENT '下注金额',
|
||||||
|
odds DECIMAL(5,2) NOT NULL COMMENT '赔率',
|
||||||
|
is_fake TINYINT DEFAULT 0 COMMENT '是否假人下注',
|
||||||
|
status TINYINT DEFAULT 0 COMMENT '状态:0待结算 1已结算 2未中奖',
|
||||||
|
win_amount BIGINT DEFAULT 0 COMMENT '中奖金额',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_user_round (user_id, round_id),
|
||||||
|
INDEX idx_round (round_id),
|
||||||
|
INDEX idx_client_msg (client_msg_id),
|
||||||
|
INDEX idx_group_id (group_id)
|
||||||
|
) COMMENT '注单表';
|
||||||
|
|
||||||
|
-- 红包表
|
||||||
|
CREATE TABLE IF NOT EXISTS red_packet (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
group_id BIGINT NOT NULL COMMENT '群ID',
|
||||||
|
round_id BIGINT COMMENT '绑定局ID(null=普通红包)',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '发送者ID',
|
||||||
|
type VARCHAR(20) NOT NULL COMMENT '类型:game游戏/random随机',
|
||||||
|
total_amount DECIMAL(10,2) NOT NULL COMMENT '总金额',
|
||||||
|
total_count INT NOT NULL COMMENT '总个数',
|
||||||
|
remain_amount DECIMAL(10,2) NOT NULL COMMENT '剩余金额',
|
||||||
|
remain_count INT NOT NULL COMMENT '剩余个数',
|
||||||
|
lucky_amount DECIMAL(10,2) COMMENT '手气王金额',
|
||||||
|
lucky_user_id BIGINT COMMENT '手气王用户ID',
|
||||||
|
lucky_digit INT COMMENT '手气王尾数',
|
||||||
|
status TINYINT DEFAULT 0 COMMENT '状态:0进行中 1已领完 2已退款',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_round (round_id),
|
||||||
|
INDEX idx_group_status (group_id, status),
|
||||||
|
INDEX idx_user (user_id)
|
||||||
|
) COMMENT '红包表';
|
||||||
|
|
||||||
|
-- 红包领取表
|
||||||
|
CREATE TABLE IF NOT EXISTS red_packet_recv (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
rp_id BIGINT NOT NULL COMMENT '红包ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '领取者ID',
|
||||||
|
amount DECIMAL(10,2) NOT NULL COMMENT '领取金额',
|
||||||
|
is_lucky TINYINT DEFAULT 0 COMMENT '是否手气王',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_rp_user (rp_id, user_id),
|
||||||
|
INDEX idx_user (user_id)
|
||||||
|
) COMMENT '红包领取表';
|
||||||
|
|
||||||
|
-- 开奖记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS draw_record (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
group_id BIGINT NOT NULL COMMENT '群ID',
|
||||||
|
round_no VARCHAR(20) NOT NULL COMMENT '期号',
|
||||||
|
round_id BIGINT NOT NULL COMMENT '局ID',
|
||||||
|
digit INT NOT NULL COMMENT '开奖尾数:0-9',
|
||||||
|
play_wins VARCHAR(100) COMMENT '中奖玩法:单,大,数字7',
|
||||||
|
total_bet INT DEFAULT 0 COMMENT '总下注',
|
||||||
|
total_award BIGINT DEFAULT 0 COMMENT '总派奖',
|
||||||
|
profit BIGINT DEFAULT 0 COMMENT '庄家利润',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_group_round (group_id, round_no),
|
||||||
|
INDEX idx_group_time (group_id, create_time)
|
||||||
|
) COMMENT '开奖记录表';
|
||||||
|
|
||||||
|
-- 尾数控制白名单表
|
||||||
|
CREATE TABLE IF NOT EXISTS digit_white (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id BIGINT NOT NULL UNIQUE COMMENT '用户ID',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_user (user_id)
|
||||||
|
) COMMENT '尾数控制白名单';
|
||||||
|
|
||||||
|
-- 审计日志表
|
||||||
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
group_id BIGINT COMMENT '群ID',
|
||||||
|
round_id BIGINT COMMENT '局ID',
|
||||||
|
user_id BIGINT COMMENT '操作用户ID',
|
||||||
|
action VARCHAR(50) NOT NULL COMMENT '动作类型',
|
||||||
|
detail JSON COMMENT '详情JSON',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_group_round (group_id, round_id),
|
||||||
|
INDEX idx_user_time (user_id, create_time),
|
||||||
|
INDEX idx_action (action)
|
||||||
|
) COMMENT '审计日志表';
|
||||||
|
|
||||||
|
-- 系统配置表
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_config (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
cfg_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置键',
|
||||||
|
cfg_value VARCHAR(500) NOT NULL COMMENT '配置值',
|
||||||
|
remark VARCHAR(255) DEFAULT '' COMMENT '备注',
|
||||||
|
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) COMMENT '系统配置表';
|
||||||
|
|
||||||
|
-- 初始化系统配置
|
||||||
|
INSERT INTO sys_config (cfg_key, cfg_value, remark) VALUES
|
||||||
|
('bet_min', '1', '单笔最小下注'),
|
||||||
|
('bet_max', '2000', '单笔最大下注'),
|
||||||
|
('bet_round_max', '20000', '单局最大下注'),
|
||||||
|
('round_interval', '60', '接注间隔(秒)'),
|
||||||
|
('send_rp_delay', '20', '结束下注后多少秒开始发红包'),
|
||||||
|
('auto_rp_timeout', '10', '开始发红包后多少秒无人发则机器人发'),
|
||||||
|
('fake_recv_timeout', '5', '红包发出后多少秒未领完托代领'),
|
||||||
|
('auto_next_delay', '5', '开奖后多少秒自动下一期'),
|
||||||
|
('odds_single_double', '1', '单双赔率'),
|
||||||
|
('odds_size', '1', '大小赔率'),
|
||||||
|
('odds_digit', '2', '数字赔率'),
|
||||||
|
('bot_initial_points', '1000000', '机器人初始积分'),
|
||||||
|
('fake_min_points', '1000', '假人初始积分最小值'),
|
||||||
|
('fake_max_points', '50000', '假人初始积分最大值'),
|
||||||
|
('fake_pool_min', '60', '假人池最小数量'),
|
||||||
|
('fake_pool_max', '120', '假人池最大数量'),
|
||||||
|
('fake_daily_active_min', '15', '每日活跃假人最小数量'),
|
||||||
|
('fake_daily_active_max', '20', '每日活跃假人最大数量'),
|
||||||
|
('win_profit_threshold', '500', '小赢利润阈值'),
|
||||||
|
('lose_profit_threshold', '-500', '小输利润阈值');
|
||||||
Reference in New Issue
Block a user