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