From 1303bf37dc8dc4f441986c570407046eff8e1e4d Mon Sep 17 00:00:00 2001
From: wells <123456@qq.com>
Date: Wed, 5 Aug 2026 13:48:48 +0800
Subject: [PATCH] init
---
pom.xml | 117 ++++++
.../com/tailbet/GuessGameApplication.java | 16 +
.../com/tailbet/config/AuthInterceptor.java | 99 ++++++
.../config/GlobalExceptionHandler.java | 26 ++
.../java/com/tailbet/config/OpenIMConfig.java | 34 ++
.../java/com/tailbet/config/RedisConfig.java | 26 ++
.../tailbet/config/RestTemplateConfig.java | 17 +
.../java/com/tailbet/config/WebMvcConfig.java | 36 ++
.../tailbet/controller/GameController.java | 122 +++++++
.../com/tailbet/controller/OpsController.java | 69 ++++
.../controller/RedPacketController.java | 58 +++
.../tailbet/controller/UserController.java | 80 +++++
src/main/java/com/tailbet/job/GameJob.java | 334 +++++++++++++++++
.../com/tailbet/mapper/AuditLogMapper.java | 12 +
.../com/tailbet/mapper/BetOrderMapper.java | 12 +
.../com/tailbet/mapper/DigitWhiteMapper.java | 12 +
.../com/tailbet/mapper/DrawRecordMapper.java | 12 +
.../com/tailbet/mapper/GameRoundMapper.java | 12 +
.../java/com/tailbet/mapper/GroupMapper.java | 12 +
.../com/tailbet/mapper/GroupMemberMapper.java | 12 +
.../com/tailbet/mapper/PointsFlowMapper.java | 12 +
.../com/tailbet/mapper/RedPacketMapper.java | 12 +
.../tailbet/mapper/RedPacketRecvMapper.java | 12 +
.../com/tailbet/mapper/SysConfigMapper.java | 12 +
.../java/com/tailbet/mapper/UserMapper.java | 12 +
.../java/com/tailbet/model/PageDomain.java | 23 ++
.../java/com/tailbet/model/dto/BetDTO.java | 29 ++
.../com/tailbet/model/dto/GameOperateDTO.java | 14 +
.../tailbet/model/dto/HistoryQueryDTO.java | 17 +
.../java/com/tailbet/model/dto/LoginDTO.java | 19 +
.../tailbet/model/dto/OpenIMCallbackDTO.java | 49 +++
.../java/com/tailbet/model/dto/PageParam.java | 20 ++
.../java/com/tailbet/model/dto/PointsDTO.java | 24 ++
.../com/tailbet/model/dto/PullGroupDTO.java | 19 +
.../com/tailbet/model/dto/ReceiveRpDTO.java | 14 +
.../com/tailbet/model/dto/RegisterDTO.java | 19 +
.../java/com/tailbet/model/dto/SendRpDTO.java | 30 ++
.../com/tailbet/model/dto/SetDigitDTO.java | 19 +
.../com/tailbet/model/dto/UpdateUserDTO.java | 24 ++
.../com/tailbet/model/entity/AuditLog.java | 48 +++
.../com/tailbet/model/entity/BetOrder.java | 74 ++++
.../com/tailbet/model/entity/DigitWhite.java | 28 ++
.../com/tailbet/model/entity/DrawRecord.java | 63 ++++
.../com/tailbet/model/entity/GameRound.java | 78 ++++
.../java/com/tailbet/model/entity/Group.java | 53 +++
.../com/tailbet/model/entity/GroupMember.java | 48 +++
.../com/tailbet/model/entity/PointsFlow.java | 63 ++++
.../com/tailbet/model/entity/RedPacket.java | 84 +++++
.../tailbet/model/entity/RedPacketRecv.java | 44 +++
.../com/tailbet/model/entity/SysConfig.java | 38 ++
.../java/com/tailbet/model/entity/User.java | 72 ++++
.../com/tailbet/model/vo/LoginUserVo.java | 70 ++++
src/main/java/com/tailbet/model/vo/R.java | 56 +++
.../com/tailbet/model/vo/UserContext.java | 46 +++
.../openim/OpenIMCallbackController.java | 189 ++++++++++
.../java/com/tailbet/openim/OpenIMClient.java | 152 ++++++++
.../com/tailbet/service/IAuditService.java | 21 ++
.../java/com/tailbet/service/IBetService.java | 27 ++
.../com/tailbet/service/IFakeUserService.java | 41 +++
.../com/tailbet/service/IGameService.java | 92 +++++
.../com/tailbet/service/IGroupService.java | 62 ++++
.../com/tailbet/service/IPointsService.java | 38 ++
.../com/tailbet/service/IPushService.java | 38 ++
.../tailbet/service/IRedPacketService.java | 59 +++
.../com/tailbet/service/IUserService.java | 30 ++
.../service/impl/AuditServiceImpl.java | 59 +++
.../tailbet/service/impl/BetServiceImpl.java | 153 ++++++++
.../service/impl/FakeUserServiceImpl.java | 177 +++++++++
.../tailbet/service/impl/GameServiceImpl.java | 336 ++++++++++++++++++
.../service/impl/GroupServiceImpl.java | 214 +++++++++++
.../service/impl/PointsServiceImpl.java | 111 ++++++
.../tailbet/service/impl/PushServiceImpl.java | 125 +++++++
.../service/impl/RedPacketServiceImpl.java | 205 +++++++++++
.../tailbet/service/impl/UserServiceImpl.java | 149 ++++++++
src/main/java/com/tailbet/util/JwtUtil.java | 93 +++++
.../java/com/tailbet/util/PasswordUtil.java | 29 ++
src/main/java/com/tailbet/util/RedisLock.java | 32 ++
.../java/com/tailbet/util/RedisLockUtil.java | 83 +++++
src/main/resources/application.yml | 53 +++
src/main/resources/sql/init.sql | 223 ++++++++++++
80 files changed, 5154 insertions(+)
create mode 100644 pom.xml
create mode 100644 src/main/java/com/tailbet/GuessGameApplication.java
create mode 100644 src/main/java/com/tailbet/config/AuthInterceptor.java
create mode 100644 src/main/java/com/tailbet/config/GlobalExceptionHandler.java
create mode 100644 src/main/java/com/tailbet/config/OpenIMConfig.java
create mode 100644 src/main/java/com/tailbet/config/RedisConfig.java
create mode 100644 src/main/java/com/tailbet/config/RestTemplateConfig.java
create mode 100644 src/main/java/com/tailbet/config/WebMvcConfig.java
create mode 100644 src/main/java/com/tailbet/controller/GameController.java
create mode 100644 src/main/java/com/tailbet/controller/OpsController.java
create mode 100644 src/main/java/com/tailbet/controller/RedPacketController.java
create mode 100644 src/main/java/com/tailbet/controller/UserController.java
create mode 100644 src/main/java/com/tailbet/job/GameJob.java
create mode 100644 src/main/java/com/tailbet/mapper/AuditLogMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/BetOrderMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/DigitWhiteMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/DrawRecordMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/GameRoundMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/GroupMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/GroupMemberMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/PointsFlowMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/RedPacketMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/RedPacketRecvMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/SysConfigMapper.java
create mode 100644 src/main/java/com/tailbet/mapper/UserMapper.java
create mode 100644 src/main/java/com/tailbet/model/PageDomain.java
create mode 100644 src/main/java/com/tailbet/model/dto/BetDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/GameOperateDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/HistoryQueryDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/LoginDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/OpenIMCallbackDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/PageParam.java
create mode 100644 src/main/java/com/tailbet/model/dto/PointsDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/PullGroupDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/ReceiveRpDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/RegisterDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/SendRpDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/SetDigitDTO.java
create mode 100644 src/main/java/com/tailbet/model/dto/UpdateUserDTO.java
create mode 100644 src/main/java/com/tailbet/model/entity/AuditLog.java
create mode 100644 src/main/java/com/tailbet/model/entity/BetOrder.java
create mode 100644 src/main/java/com/tailbet/model/entity/DigitWhite.java
create mode 100644 src/main/java/com/tailbet/model/entity/DrawRecord.java
create mode 100644 src/main/java/com/tailbet/model/entity/GameRound.java
create mode 100644 src/main/java/com/tailbet/model/entity/Group.java
create mode 100644 src/main/java/com/tailbet/model/entity/GroupMember.java
create mode 100644 src/main/java/com/tailbet/model/entity/PointsFlow.java
create mode 100644 src/main/java/com/tailbet/model/entity/RedPacket.java
create mode 100644 src/main/java/com/tailbet/model/entity/RedPacketRecv.java
create mode 100644 src/main/java/com/tailbet/model/entity/SysConfig.java
create mode 100644 src/main/java/com/tailbet/model/entity/User.java
create mode 100644 src/main/java/com/tailbet/model/vo/LoginUserVo.java
create mode 100644 src/main/java/com/tailbet/model/vo/R.java
create mode 100644 src/main/java/com/tailbet/model/vo/UserContext.java
create mode 100644 src/main/java/com/tailbet/openim/OpenIMCallbackController.java
create mode 100644 src/main/java/com/tailbet/openim/OpenIMClient.java
create mode 100644 src/main/java/com/tailbet/service/IAuditService.java
create mode 100644 src/main/java/com/tailbet/service/IBetService.java
create mode 100644 src/main/java/com/tailbet/service/IFakeUserService.java
create mode 100644 src/main/java/com/tailbet/service/IGameService.java
create mode 100644 src/main/java/com/tailbet/service/IGroupService.java
create mode 100644 src/main/java/com/tailbet/service/IPointsService.java
create mode 100644 src/main/java/com/tailbet/service/IPushService.java
create mode 100644 src/main/java/com/tailbet/service/IRedPacketService.java
create mode 100644 src/main/java/com/tailbet/service/IUserService.java
create mode 100644 src/main/java/com/tailbet/service/impl/AuditServiceImpl.java
create mode 100644 src/main/java/com/tailbet/service/impl/BetServiceImpl.java
create mode 100644 src/main/java/com/tailbet/service/impl/FakeUserServiceImpl.java
create mode 100644 src/main/java/com/tailbet/service/impl/GameServiceImpl.java
create mode 100644 src/main/java/com/tailbet/service/impl/GroupServiceImpl.java
create mode 100644 src/main/java/com/tailbet/service/impl/PointsServiceImpl.java
create mode 100644 src/main/java/com/tailbet/service/impl/PushServiceImpl.java
create mode 100644 src/main/java/com/tailbet/service/impl/RedPacketServiceImpl.java
create mode 100644 src/main/java/com/tailbet/service/impl/UserServiceImpl.java
create mode 100644 src/main/java/com/tailbet/util/JwtUtil.java
create mode 100644 src/main/java/com/tailbet/util/PasswordUtil.java
create mode 100644 src/main/java/com/tailbet/util/RedisLock.java
create mode 100644 src/main/java/com/tailbet/util/RedisLockUtil.java
create mode 100644 src/main/resources/application.yml
create mode 100644 src/main/resources/sql/init.sql
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..1894bc0
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,117 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.5
+
+
+
+ com.tailbet
+ guess-game-backend
+ 1.0.0
+ jar
+ guess-game-backend
+ 尾数竞猜游戏后端
+
+
+ 17
+ 3.5.9
+ 2023.0.3
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ ${mybatis-plus.version}
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ cn.hutool
+ hutool-all
+ 5.8.30
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.6
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.6
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.6
+ runtime
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/src/main/java/com/tailbet/GuessGameApplication.java b/src/main/java/com/tailbet/GuessGameApplication.java
new file mode 100644
index 0000000..5aae68b
--- /dev/null
+++ b/src/main/java/com/tailbet/GuessGameApplication.java
@@ -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);
+ }
+}
diff --git a/src/main/java/com/tailbet/config/AuthInterceptor.java b/src/main/java/com/tailbet/config/AuthInterceptor.java
new file mode 100644
index 0000000..23a04aa
--- /dev/null
+++ b/src/main/java/com/tailbet/config/AuthInterceptor.java
@@ -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));
+ }
+}
diff --git a/src/main/java/com/tailbet/config/GlobalExceptionHandler.java b/src/main/java/com/tailbet/config/GlobalExceptionHandler.java
new file mode 100644
index 0000000..a94f6f6
--- /dev/null
+++ b/src/main/java/com/tailbet/config/GlobalExceptionHandler.java
@@ -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());
+ }
+}
diff --git a/src/main/java/com/tailbet/config/OpenIMConfig.java b/src/main/java/com/tailbet/config/OpenIMConfig.java
new file mode 100644
index 0000000..589c301
--- /dev/null
+++ b/src/main/java/com/tailbet/config/OpenIMConfig.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/config/RedisConfig.java b/src/main/java/com/tailbet/config/RedisConfig.java
new file mode 100644
index 0000000..484963a
--- /dev/null
+++ b/src/main/java/com/tailbet/config/RedisConfig.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/tailbet/config/RestTemplateConfig.java b/src/main/java/com/tailbet/config/RestTemplateConfig.java
new file mode 100644
index 0000000..d9520a1
--- /dev/null
+++ b/src/main/java/com/tailbet/config/RestTemplateConfig.java
@@ -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();
+ }
+}
diff --git a/src/main/java/com/tailbet/config/WebMvcConfig.java b/src/main/java/com/tailbet/config/WebMvcConfig.java
new file mode 100644
index 0000000..1b77d6a
--- /dev/null
+++ b/src/main/java/com/tailbet/config/WebMvcConfig.java
@@ -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");
+ }
+}
diff --git a/src/main/java/com/tailbet/controller/GameController.java b/src/main/java/com/tailbet/controller/GameController.java
new file mode 100644
index 0000000..cd77165
--- /dev/null
+++ b/src/main/java/com/tailbet/controller/GameController.java
@@ -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 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());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/tailbet/controller/OpsController.java b/src/main/java/com/tailbet/controller/OpsController.java
new file mode 100644
index 0000000..2d5c0bc
--- /dev/null
+++ b/src/main/java/com/tailbet/controller/OpsController.java
@@ -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());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/tailbet/controller/RedPacketController.java b/src/main/java/com/tailbet/controller/RedPacketController.java
new file mode 100644
index 0000000..2f47eed
--- /dev/null
+++ b/src/main/java/com/tailbet/controller/RedPacketController.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/tailbet/controller/UserController.java b/src/main/java/com/tailbet/controller/UserController.java
new file mode 100644
index 0000000..b0c39c2
--- /dev/null
+++ b/src/main/java/com/tailbet/controller/UserController.java
@@ -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("退出成功");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/tailbet/job/GameJob.java b/src/main/java/com/tailbet/job/GameJob.java
new file mode 100644
index 0000000..942866f
--- /dev/null
+++ b/src/main/java/com/tailbet/job/GameJob.java
@@ -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 roundTasks = new ConcurrentHashMap<>();
+
+ /**
+ * 检查红包是否需要托领取(每3秒执行)
+ */
+ @Scheduled(fixedDelay = 3000)
+ public void checkRedPacketForFakeReceive() {
+ // 查找进行中的游戏红包
+ List ongoingRps = redPacketMapper.selectList(new LambdaQueryWrapper()
+ .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 fakes = fakeUserService.getFakeUsers();
+ if (fakes.isEmpty()) {
+ return;
+ }
+
+ // 随机选一个假人
+ User fake = fakes.get((int) (Math.random() * fakes.size()));
+
+ // 检查是否已经领取过
+ var existing = redPacketMapper.selectList(new LambdaQueryWrapper()
+ .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 ongoingRounds = gameRoundMapper.selectList(new LambdaQueryWrapper()
+ .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()
+ .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 whites = digitWhiteMapper.selectList(new LambdaQueryWrapper());
+ 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;
+ }
+}
diff --git a/src/main/java/com/tailbet/mapper/AuditLogMapper.java b/src/main/java/com/tailbet/mapper/AuditLogMapper.java
new file mode 100644
index 0000000..51efd58
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/AuditLogMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/BetOrderMapper.java b/src/main/java/com/tailbet/mapper/BetOrderMapper.java
new file mode 100644
index 0000000..a82ecd6
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/BetOrderMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/DigitWhiteMapper.java b/src/main/java/com/tailbet/mapper/DigitWhiteMapper.java
new file mode 100644
index 0000000..385bcb2
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/DigitWhiteMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/DrawRecordMapper.java b/src/main/java/com/tailbet/mapper/DrawRecordMapper.java
new file mode 100644
index 0000000..895a052
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/DrawRecordMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/GameRoundMapper.java b/src/main/java/com/tailbet/mapper/GameRoundMapper.java
new file mode 100644
index 0000000..fbc41f7
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/GameRoundMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/GroupMapper.java b/src/main/java/com/tailbet/mapper/GroupMapper.java
new file mode 100644
index 0000000..23f4c1f
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/GroupMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/GroupMemberMapper.java b/src/main/java/com/tailbet/mapper/GroupMemberMapper.java
new file mode 100644
index 0000000..448ad02
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/GroupMemberMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/PointsFlowMapper.java b/src/main/java/com/tailbet/mapper/PointsFlowMapper.java
new file mode 100644
index 0000000..dcb933d
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/PointsFlowMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/RedPacketMapper.java b/src/main/java/com/tailbet/mapper/RedPacketMapper.java
new file mode 100644
index 0000000..7c20438
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/RedPacketMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/RedPacketRecvMapper.java b/src/main/java/com/tailbet/mapper/RedPacketRecvMapper.java
new file mode 100644
index 0000000..b3d550b
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/RedPacketRecvMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/SysConfigMapper.java b/src/main/java/com/tailbet/mapper/SysConfigMapper.java
new file mode 100644
index 0000000..c9a4725
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/SysConfigMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/mapper/UserMapper.java b/src/main/java/com/tailbet/mapper/UserMapper.java
new file mode 100644
index 0000000..4470730
--- /dev/null
+++ b/src/main/java/com/tailbet/mapper/UserMapper.java
@@ -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 {
+}
diff --git a/src/main/java/com/tailbet/model/PageDomain.java b/src/main/java/com/tailbet/model/PageDomain.java
new file mode 100644
index 0000000..de8b11f
--- /dev/null
+++ b/src/main/java/com/tailbet/model/PageDomain.java
@@ -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 Page toPage() {
+ return new Page<>(pageNum, pageSize);
+
+ }
+}
diff --git a/src/main/java/com/tailbet/model/dto/BetDTO.java b/src/main/java/com/tailbet/model/dto/BetDTO.java
new file mode 100644
index 0000000..598f6f8
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/BetDTO.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/dto/GameOperateDTO.java b/src/main/java/com/tailbet/model/dto/GameOperateDTO.java
new file mode 100644
index 0000000..cdfda12
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/GameOperateDTO.java
@@ -0,0 +1,14 @@
+package com.tailbet.model.dto;
+
+import lombok.Data;
+
+/**
+ * 游戏操作请求
+ */
+@Data
+public class GameOperateDTO {
+ /**
+ * 群ID
+ */
+ private Long groupId;
+}
diff --git a/src/main/java/com/tailbet/model/dto/HistoryQueryDTO.java b/src/main/java/com/tailbet/model/dto/HistoryQueryDTO.java
new file mode 100644
index 0000000..8e663e9
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/HistoryQueryDTO.java
@@ -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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/tailbet/model/dto/LoginDTO.java b/src/main/java/com/tailbet/model/dto/LoginDTO.java
new file mode 100644
index 0000000..34b25c0
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/LoginDTO.java
@@ -0,0 +1,19 @@
+package com.tailbet.model.dto;
+
+import lombok.Data;
+
+/**
+ * 用户登录请求
+ */
+@Data
+public class LoginDTO {
+ /**
+ * 账号
+ */
+ private String username;
+
+ /**
+ * 密码
+ */
+ private String password;
+}
diff --git a/src/main/java/com/tailbet/model/dto/OpenIMCallbackDTO.java b/src/main/java/com/tailbet/model/dto/OpenIMCallbackDTO.java
new file mode 100644
index 0000000..8545d91
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/OpenIMCallbackDTO.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/dto/PageParam.java b/src/main/java/com/tailbet/model/dto/PageParam.java
new file mode 100644
index 0000000..d819741
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/PageParam.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/dto/PointsDTO.java b/src/main/java/com/tailbet/model/dto/PointsDTO.java
new file mode 100644
index 0000000..075b5ef
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/PointsDTO.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/dto/PullGroupDTO.java b/src/main/java/com/tailbet/model/dto/PullGroupDTO.java
new file mode 100644
index 0000000..02e9129
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/PullGroupDTO.java
@@ -0,0 +1,19 @@
+package com.tailbet.model.dto;
+
+import lombok.Data;
+
+/**
+ * 拉群请求
+ */
+@Data
+public class PullGroupDTO {
+ /**
+ * 用户ID
+ */
+ private Long userId;
+
+ /**
+ * 群ID
+ */
+ private Long groupId;
+}
diff --git a/src/main/java/com/tailbet/model/dto/ReceiveRpDTO.java b/src/main/java/com/tailbet/model/dto/ReceiveRpDTO.java
new file mode 100644
index 0000000..50ca399
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/ReceiveRpDTO.java
@@ -0,0 +1,14 @@
+package com.tailbet.model.dto;
+
+import lombok.Data;
+
+/**
+ * 领红包请求
+ */
+@Data
+public class ReceiveRpDTO {
+ /**
+ * 红包ID
+ */
+ private Long rpId;
+}
diff --git a/src/main/java/com/tailbet/model/dto/RegisterDTO.java b/src/main/java/com/tailbet/model/dto/RegisterDTO.java
new file mode 100644
index 0000000..45c1738
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/RegisterDTO.java
@@ -0,0 +1,19 @@
+package com.tailbet.model.dto;
+
+import lombok.Data;
+
+/**
+ * 用户注册请求
+ */
+@Data
+public class RegisterDTO {
+ /**
+ * 账号
+ */
+ private String username;
+
+ /**
+ * 密码
+ */
+ private String password;
+}
diff --git a/src/main/java/com/tailbet/model/dto/SendRpDTO.java b/src/main/java/com/tailbet/model/dto/SendRpDTO.java
new file mode 100644
index 0000000..4a795f2
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/SendRpDTO.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/dto/SetDigitDTO.java b/src/main/java/com/tailbet/model/dto/SetDigitDTO.java
new file mode 100644
index 0000000..cdb41d2
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/SetDigitDTO.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/dto/UpdateUserDTO.java b/src/main/java/com/tailbet/model/dto/UpdateUserDTO.java
new file mode 100644
index 0000000..75e5f26
--- /dev/null
+++ b/src/main/java/com/tailbet/model/dto/UpdateUserDTO.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/AuditLog.java b/src/main/java/com/tailbet/model/entity/AuditLog.java
new file mode 100644
index 0000000..b8d9e66
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/AuditLog.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/BetOrder.java b/src/main/java/com/tailbet/model/entity/BetOrder.java
new file mode 100644
index 0000000..5c9fbc6
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/BetOrder.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/DigitWhite.java b/src/main/java/com/tailbet/model/entity/DigitWhite.java
new file mode 100644
index 0000000..b4ec5bc
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/DigitWhite.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/DrawRecord.java b/src/main/java/com/tailbet/model/entity/DrawRecord.java
new file mode 100644
index 0000000..61b4602
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/DrawRecord.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/GameRound.java b/src/main/java/com/tailbet/model/entity/GameRound.java
new file mode 100644
index 0000000..9c3083c
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/GameRound.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/Group.java b/src/main/java/com/tailbet/model/entity/Group.java
new file mode 100644
index 0000000..66ba88f
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/Group.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/GroupMember.java b/src/main/java/com/tailbet/model/entity/GroupMember.java
new file mode 100644
index 0000000..e0f13cc
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/GroupMember.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/PointsFlow.java b/src/main/java/com/tailbet/model/entity/PointsFlow.java
new file mode 100644
index 0000000..e4b0a55
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/PointsFlow.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/RedPacket.java b/src/main/java/com/tailbet/model/entity/RedPacket.java
new file mode 100644
index 0000000..3db30f8
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/RedPacket.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/RedPacketRecv.java b/src/main/java/com/tailbet/model/entity/RedPacketRecv.java
new file mode 100644
index 0000000..41c60f7
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/RedPacketRecv.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/SysConfig.java b/src/main/java/com/tailbet/model/entity/SysConfig.java
new file mode 100644
index 0000000..eaf2f96
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/SysConfig.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/entity/User.java b/src/main/java/com/tailbet/model/entity/User.java
new file mode 100644
index 0000000..327a766
--- /dev/null
+++ b/src/main/java/com/tailbet/model/entity/User.java
@@ -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;
+}
diff --git a/src/main/java/com/tailbet/model/vo/LoginUserVo.java b/src/main/java/com/tailbet/model/vo/LoginUserVo.java
new file mode 100644
index 0000000..7831fe6
--- /dev/null
+++ b/src/main/java/com/tailbet/model/vo/LoginUserVo.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/tailbet/model/vo/R.java b/src/main/java/com/tailbet/model/vo/R.java
new file mode 100644
index 0000000..1978560
--- /dev/null
+++ b/src/main/java/com/tailbet/model/vo/R.java
@@ -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 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 R ok() {
+ return new R<>(SUCCESS, "操作成功", null);
+ }
+
+ public static R ok(T data) {
+ return new R<>(SUCCESS, "操作成功", data);
+ }
+
+ public static R ok(String msg, T data) {
+ return new R<>(SUCCESS, msg, data);
+ }
+
+ public static R fail() {
+ return new R<>(FAIL, "操作失败", null);
+ }
+
+ public static R fail(String msg) {
+ return new R<>(FAIL, msg, null);
+ }
+
+ public static R fail(int code, String msg) {
+ return new R<>(code, msg, null);
+ }
+
+ public boolean isSuccess() {
+ return code == SUCCESS;
+ }
+}
diff --git a/src/main/java/com/tailbet/model/vo/UserContext.java b/src/main/java/com/tailbet/model/vo/UserContext.java
new file mode 100644
index 0000000..2c03a0f
--- /dev/null
+++ b/src/main/java/com/tailbet/model/vo/UserContext.java
@@ -0,0 +1,46 @@
+package com.tailbet.model.vo;
+
+/**
+ * 用户上下文 - ThreadLocal保存当前登录用户
+ */
+public class UserContext {
+
+ private static final ThreadLocal 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());
+ }
+}
diff --git a/src/main/java/com/tailbet/openim/OpenIMCallbackController.java b/src/main/java/com/tailbet/openim/OpenIMCallbackController.java
new file mode 100644
index 0000000..d98258b
--- /dev/null
+++ b/src/main/java/com/tailbet/openim/OpenIMCallbackController.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/tailbet/openim/OpenIMClient.java b/src/main/java/com/tailbet/openim/OpenIMClient.java
new file mode 100644
index 0000000..516a30c
--- /dev/null
+++ b/src/main/java/com/tailbet/openim/OpenIMClient.java
@@ -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 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