Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df5daf5910 | ||
|
|
1c5e9d4cde | ||
|
|
dc98850b4d |
@@ -1,20 +1,17 @@
|
|||||||
package com.tailbet.controller;
|
package com.tailbet.controller;
|
||||||
|
|
||||||
import com.tailbet.job.GameJob;
|
import com.tailbet.job.GameJob;
|
||||||
import com.tailbet.mapper.DigitWhiteMapper;
|
|
||||||
import com.tailbet.model.dto.BetDTO;
|
import com.tailbet.model.dto.BetDTO;
|
||||||
import com.tailbet.model.dto.GameOperateDTO;
|
import com.tailbet.model.dto.GameOperateDTO;
|
||||||
import com.tailbet.model.dto.HistoryQueryDTO;
|
import com.tailbet.model.dto.HistoryQueryDTO;
|
||||||
import com.tailbet.model.dto.SetDigitDTO;
|
import com.tailbet.model.dto.SetDigitDTO;
|
||||||
import com.tailbet.model.entity.BetOrder;
|
import com.tailbet.model.entity.BetOrder;
|
||||||
import com.tailbet.model.entity.DigitWhite;
|
|
||||||
import com.tailbet.model.entity.GameRound;
|
import com.tailbet.model.entity.GameRound;
|
||||||
import com.tailbet.model.vo.DigitTrialVo;
|
import com.tailbet.model.vo.DigitTrialVo;
|
||||||
import com.tailbet.model.vo.R;
|
import com.tailbet.model.vo.R;
|
||||||
import com.tailbet.model.vo.UserContext;
|
import com.tailbet.model.vo.UserContext;
|
||||||
import com.tailbet.service.IBetService;
|
import com.tailbet.service.IBetService;
|
||||||
import com.tailbet.service.IGameService;
|
import com.tailbet.service.IGameService;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -32,7 +29,6 @@ public class GameController {
|
|||||||
private final IGameService gameService;
|
private final IGameService gameService;
|
||||||
private final IBetService betService;
|
private final IBetService betService;
|
||||||
private final GameJob gameJob;
|
private final GameJob gameJob;
|
||||||
private final DigitWhiteMapper digitWhiteMapper;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始游戏(群主)
|
* 开始游戏(群主)
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package com.tailbet.job;
|
||||||
|
|
||||||
|
import com.tailbet.config.OpenIMProperties;
|
||||||
|
import com.tailbet.mapper.DrawRecordMapper;
|
||||||
|
import com.tailbet.mapper.GameRoundMapper;
|
||||||
|
import com.tailbet.mapper.GroupMemberMapper;
|
||||||
|
import com.tailbet.mapper.UserMapper;
|
||||||
|
import com.tailbet.model.entity.DrawRecord;
|
||||||
|
import com.tailbet.model.entity.GameRound;
|
||||||
|
import com.tailbet.model.entity.GroupMember;
|
||||||
|
import com.tailbet.model.entity.User;
|
||||||
|
import com.tailbet.openim.OpenIMApiClient;
|
||||||
|
import com.tailbet.service.IGameService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏广播器 —— 把 OpenIM 消息发送逻辑从 {@link GameJob} 抽出
|
||||||
|
*
|
||||||
|
* <p>负责所有游戏流程的 OpenIM 消息:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #broadcastStartBet} —— 开始下注</li>
|
||||||
|
* <li>{@link #broadcastEndBet} —— 结束下注</li>
|
||||||
|
* <li>{@link #broadcastStartRp} —— 开始发红包</li>
|
||||||
|
* <li>{@link #broadcastResult} —— 开奖结果(含近30期历史)</li>
|
||||||
|
* <li>{@link #notifyWhiteUsers} —— 通知尾数控制白名单</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>所有方法失败时只记日志、不抛异常,避免阻塞调用方业务流程。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GameBroadcaster {
|
||||||
|
|
||||||
|
/** sessionType: 1=单聊 */
|
||||||
|
private static final int SESSION_TYPE_C2C = 1;
|
||||||
|
/** sessionType: 3=群聊 */
|
||||||
|
private static final int SESSION_TYPE_GROUP = 3;
|
||||||
|
/** contentType: 101=文本 */
|
||||||
|
private static final int CONTENT_TYPE_TEXT = 101;
|
||||||
|
|
||||||
|
private final OpenIMApiClient openIMApiClient;
|
||||||
|
private final OpenIMProperties openIMProperties;
|
||||||
|
private final GameRoundMapper gameRoundMapper;
|
||||||
|
private final DrawRecordMapper drawRecordMapper;
|
||||||
|
private final UserMapper userMapper;
|
||||||
|
private final GroupMemberMapper groupMemberMapper;
|
||||||
|
private final IGameService gameService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知当前群的白名单用户(尾数试算完成)
|
||||||
|
* <p>白名单以 {@code user.is_white=1} 为权威源,与本群成员({@code group_member})取交集后通知。
|
||||||
|
*/
|
||||||
|
public void notifyWhiteUsers(Long roundId) {
|
||||||
|
try {
|
||||||
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
|
if (round == null) return;
|
||||||
|
Long groupId = round.getGroupId();
|
||||||
|
|
||||||
|
// 1. 取本群成员用户ID集合(逻辑删除由全局配置自动过滤)
|
||||||
|
Set<Long> memberIds = groupMemberMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<GroupMember>()
|
||||||
|
.eq(GroupMember::getGroupId, groupId)
|
||||||
|
.select(GroupMember::getUserId))
|
||||||
|
.stream()
|
||||||
|
.map(GroupMember::getUserId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (memberIds.isEmpty()) {
|
||||||
|
log.debug("本局{}所属群{}无成员,跳过白名单通知", roundId, groupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 取白名单用户ID集合(user.is_white = 1)
|
||||||
|
Set<Long> whiteIds = userMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<User>()
|
||||||
|
.eq(User::getIsWhite, 1)
|
||||||
|
.select(User::getId))
|
||||||
|
.stream()
|
||||||
|
.map(User::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (whiteIds.isEmpty()) {
|
||||||
|
log.debug("无白名单用户,跳过通知: roundId={}", roundId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 取交集:本群成员 � 白名单
|
||||||
|
memberIds.retainAll(whiteIds);
|
||||||
|
if (memberIds.isEmpty()) {
|
||||||
|
log.debug("本局{}所属群{}无白名单成员,跳过通知", roundId, groupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String message = "尾数试算完成,请前往修改尾数。期号: " + round.getRoundNo();
|
||||||
|
for (Long userId : memberIds) {
|
||||||
|
sendC2C(userId, message);
|
||||||
|
log.info("通知白名单用户尾数试算完成: userId={}, groupId={}, roundId={}", userId, groupId, roundId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("通知白名单用户失败: roundId={}, error={}", roundId, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播开始下注
|
||||||
|
*/
|
||||||
|
public void broadcastStartBet(Long groupId, String roundNo) {
|
||||||
|
sendGroup(groupId, roundNo + "期开始下注");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播结束下注
|
||||||
|
*/
|
||||||
|
public void broadcastEndBet(Long groupId, String roundNo) {
|
||||||
|
sendGroup(groupId, roundNo + "期结束下注");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播开始发红包
|
||||||
|
*/
|
||||||
|
public void broadcastStartRp(Long groupId, String roundNo) {
|
||||||
|
sendGroup(groupId, roundNo + "期开始发红包");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播开奖结果(含近30期历史)
|
||||||
|
*/
|
||||||
|
public void broadcastResult(Long groupId, int digit, String roundNo) {
|
||||||
|
try {
|
||||||
|
String playWinDesc = gameService.getPlayWinDesc(digit);
|
||||||
|
StringBuilder message = new StringBuilder()
|
||||||
|
.append(roundNo).append("期开奖\n")
|
||||||
|
.append("尾数: ").append(digit).append("\n")
|
||||||
|
.append("中奖玩法: ").append(playWinDesc);
|
||||||
|
|
||||||
|
String history = getRecentHistory(groupId);
|
||||||
|
if (!history.isEmpty()) {
|
||||||
|
message.append("\n\n近30期:\n").append(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendGroup(groupId, message.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("广播开奖结果失败: groupId={}, digit={}, error={}", groupId, digit, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取近30期历史开奖记录
|
||||||
|
*/
|
||||||
|
private String getRecentHistory(Long groupId) {
|
||||||
|
List<DrawRecord> records = drawRecordMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<DrawRecord>()
|
||||||
|
.eq(DrawRecord::getGroupId, groupId)
|
||||||
|
.orderByDesc(DrawRecord::getCreateTime)
|
||||||
|
.last("LIMIT 30"));
|
||||||
|
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (DrawRecord record : records) {
|
||||||
|
sb.append(record.getRoundNo()).append(":").append(record.getDigit()).append("\n");
|
||||||
|
}
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 底层发送封装 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送群聊文本消息
|
||||||
|
*/
|
||||||
|
private void sendGroup(Long groupId, String content) {
|
||||||
|
try {
|
||||||
|
// 注意: 实际生产中需要通过Group表映射获取OpenIM群ID
|
||||||
|
openIMApiClient.sendMsg(
|
||||||
|
openIMProperties.getAdminUserId(),
|
||||||
|
String.valueOf(groupId),
|
||||||
|
SESSION_TYPE_GROUP,
|
||||||
|
CONTENT_TYPE_TEXT,
|
||||||
|
java.util.Map.of("content", content)
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("群消息发送失败: groupId={}, content={}, error={}", groupId, content, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送单聊文本消息
|
||||||
|
*/
|
||||||
|
private void sendC2C(Long userId, String content) {
|
||||||
|
try {
|
||||||
|
openIMApiClient.sendMsg(
|
||||||
|
openIMProperties.getAdminUserId(),
|
||||||
|
String.valueOf(userId),
|
||||||
|
SESSION_TYPE_C2C,
|
||||||
|
CONTENT_TYPE_TEXT,
|
||||||
|
java.util.Map.of("content", content)
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("单聊消息发送失败: userId={}, content={}, error={}", userId, content, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,17 @@
|
|||||||
package com.tailbet.job;
|
package com.tailbet.job;
|
||||||
|
|
||||||
import com.tailbet.model.RoundTask;
|
|
||||||
import com.tailbet.model.entity.GameRound;
|
import com.tailbet.model.entity.GameRound;
|
||||||
import com.tailbet.model.entity.RedPacket;
|
import com.tailbet.model.entity.RedPacket;
|
||||||
import com.tailbet.model.entity.User;
|
import com.tailbet.model.entity.User;
|
||||||
import com.tailbet.mapper.DrawRecordMapper;
|
|
||||||
import com.tailbet.mapper.DigitWhiteMapper;
|
|
||||||
import com.tailbet.mapper.GameRoundMapper;
|
import com.tailbet.mapper.GameRoundMapper;
|
||||||
import com.tailbet.mapper.GroupMemberMapper;
|
import com.tailbet.mapper.GroupMemberMapper;
|
||||||
import com.tailbet.mapper.PendingTaskMapper;
|
|
||||||
import com.tailbet.mapper.RedPacketMapper;
|
import com.tailbet.mapper.RedPacketMapper;
|
||||||
import com.tailbet.mapper.RedPacketRecvMapper;
|
import com.tailbet.mapper.RedPacketRecvMapper;
|
||||||
import com.tailbet.mapper.SysConfigMapper;
|
import com.tailbet.mapper.SysConfigMapper;
|
||||||
import com.tailbet.mapper.UserMapper;
|
import com.tailbet.mapper.UserMapper;
|
||||||
import com.tailbet.model.entity.DigitWhite;
|
|
||||||
import com.tailbet.model.entity.DrawRecord;
|
|
||||||
import com.tailbet.model.entity.GroupMember;
|
import com.tailbet.model.entity.GroupMember;
|
||||||
import com.tailbet.model.entity.RedPacketRecv;
|
import com.tailbet.model.entity.RedPacketRecv;
|
||||||
import com.tailbet.model.entity.SysConfig;
|
import com.tailbet.model.entity.SysConfig;
|
||||||
import com.tailbet.model.entity.PendingTask;
|
|
||||||
import com.tailbet.model.PendingTaskInfo;
|
|
||||||
import com.tailbet.openim.OpenIMApiClient;
|
|
||||||
import com.tailbet.service.IGameService;
|
import com.tailbet.service.IGameService;
|
||||||
import com.tailbet.service.IRedPacketService;
|
import com.tailbet.service.IRedPacketService;
|
||||||
import com.tailbet.service.IFakeUserService;
|
import com.tailbet.service.IFakeUserService;
|
||||||
@@ -40,6 +31,19 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏定时任务
|
* 游戏定时任务
|
||||||
|
*
|
||||||
|
* <p>状态完全由 {@code game_round} 数据库表承载,不再使用内存 Map。
|
||||||
|
* 定时任务只做"数据库扫描 + 状态机推进",天然支持单实例重启恢复。
|
||||||
|
*
|
||||||
|
* <p>扫描任务(每 2 秒):
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #scanRoundTimeouts()} —— 下注超时自动封盘</li>
|
||||||
|
* <li>{@link #scanStartRpTimeouts()} —— 封盘后延迟开始发红包</li>
|
||||||
|
* <li>{@link #scanBotSendRpTimeouts()} —— 发包超时由机器人代发</li>
|
||||||
|
* <li>{@link #scanAutoNextTimeouts()} —— 结算后自动开下一期</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>所有 OpenIM 消息广播逻辑由 {@link GameBroadcaster} 负责。
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@@ -49,139 +53,34 @@ public class GameJob {
|
|||||||
private final GameRoundMapper gameRoundMapper;
|
private final GameRoundMapper gameRoundMapper;
|
||||||
private final RedPacketMapper redPacketMapper;
|
private final RedPacketMapper redPacketMapper;
|
||||||
private final RedPacketRecvMapper redPacketRecvMapper;
|
private final RedPacketRecvMapper redPacketRecvMapper;
|
||||||
private final DrawRecordMapper drawRecordMapper;
|
|
||||||
private final SysConfigMapper sysConfigMapper;
|
private final SysConfigMapper sysConfigMapper;
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
private final DigitWhiteMapper digitWhiteMapper;
|
|
||||||
private final PendingTaskMapper pendingTaskMapper;
|
|
||||||
private final GroupMemberMapper groupMemberMapper;
|
private final GroupMemberMapper groupMemberMapper;
|
||||||
private final IGameService gameService;
|
private final IGameService gameService;
|
||||||
private final IRedPacketService redPacketService;
|
private final IRedPacketService redPacketService;
|
||||||
private final IFakeUserService fakeUserService;
|
private final IFakeUserService fakeUserService;
|
||||||
private final OpenIMApiClient openIMApiClient;
|
private final GameBroadcaster broadcaster;
|
||||||
private final com.tailbet.config.OpenIMProperties openIMProperties;
|
|
||||||
|
|
||||||
// 局状态监控(使用内存缓存,实际生产应使用Redis)
|
|
||||||
private final Map<Long, RoundTask> roundTasks = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
// 配置缓存
|
// 配置缓存
|
||||||
private final Map<String, String> configCache = new ConcurrentHashMap<>();
|
private final Map<String, String> configCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// 延迟任务队列
|
|
||||||
private final Map<String, PendingTaskInfo> pendingTasks = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
// 任务ID生成器 (格式: {type}_{groupId}_{自增序号})
|
|
||||||
private java.util.concurrent.atomic.AtomicLong taskIdGenerator = new java.util.concurrent.atomic.AtomicLong(1);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动时加载配置缓存并恢复内存状态
|
* 启动时加载配置缓存
|
||||||
|
* <p>不再做 roundTasks/pendingTasks 内存恢复 —— 扫描任务启动后会自动从数据库接管
|
||||||
*/
|
*/
|
||||||
@jakarta.annotation.PostConstruct
|
@jakarta.annotation.PostConstruct
|
||||||
public void loadConfig() {
|
public void loadConfig() {
|
||||||
try {
|
try {
|
||||||
// 加载配置缓存
|
|
||||||
List<SysConfig> configs = sysConfigMapper.selectList(null);
|
List<SysConfig> configs = sysConfigMapper.selectList(null);
|
||||||
for (SysConfig config : configs) {
|
for (SysConfig config : configs) {
|
||||||
configCache.put(config.getCfgKey(), config.getCfgValue());
|
configCache.put(config.getCfgKey(), config.getCfgValue());
|
||||||
}
|
}
|
||||||
log.info("配置缓存加载完成,共 {} 条配置", configs.size());
|
log.info("配置缓存加载完成,共 {} 条配置", configs.size());
|
||||||
|
|
||||||
// 恢复进行中的局状态
|
|
||||||
recoverRoundTasks();
|
|
||||||
|
|
||||||
// 恢复延迟任务队列
|
|
||||||
recoverPendingTasks();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("启动初始化失败: {}", e.getMessage(), e);
|
log.error("启动初始化失败: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 从数据库恢复进行中的局状态
|
|
||||||
*/
|
|
||||||
private void recoverRoundTasks() {
|
|
||||||
try {
|
|
||||||
List<GameRound> ongoingRounds = gameRoundMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<GameRound>()
|
|
||||||
.lt(GameRound::getStatus, IGameService.STATUS_FINISHED));
|
|
||||||
|
|
||||||
if (ongoingRounds.isEmpty()) {
|
|
||||||
log.info("无进行中的局,无需恢复");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int bettingInterval = getConfigInt("betting_interval", 60);
|
|
||||||
|
|
||||||
for (GameRound round : ongoingRounds) {
|
|
||||||
RoundTask task = new RoundTask();
|
|
||||||
task.setRoundId(round.getId());
|
|
||||||
task.setGroupId(round.getGroupId());
|
|
||||||
|
|
||||||
// 优先使用数据库中的endBetTime,否则根据startTime计算
|
|
||||||
if (round.getEndBetTime() != null) {
|
|
||||||
task.setEndBetTime(round.getEndBetTime());
|
|
||||||
} else if (round.getStartTime() != null) {
|
|
||||||
task.setEndBetTime(round.getStartTime().plusSeconds(bettingInterval));
|
|
||||||
} else {
|
|
||||||
// 兜底:使用配置的下注时长
|
|
||||||
task.setEndBetTime(LocalDateTime.now().plusSeconds(bettingInterval));
|
|
||||||
}
|
|
||||||
|
|
||||||
roundTasks.put(round.getId(), task);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("局状态恢复完成,共恢复 {} 个进行中的局", ongoingRounds.size());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("恢复局状态失败: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从数据库恢复延迟任务队列
|
|
||||||
* 注:包括已过期的任务,过期任务会在processPendingTasks中立即执行
|
|
||||||
*/
|
|
||||||
private void recoverPendingTasks() {
|
|
||||||
try {
|
|
||||||
// 数据库中残留的记录都是未执行的(执行成功后会立即删除),全部恢复
|
|
||||||
List<PendingTask> dbTasks = pendingTaskMapper.selectList(null);
|
|
||||||
|
|
||||||
if (dbTasks == null || dbTasks.isEmpty()) {
|
|
||||||
log.info("无待执行的延迟任务,无需恢复");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
|
||||||
int recovered = 0;
|
|
||||||
int expiredCount = 0;
|
|
||||||
for (PendingTask dbTask : dbTasks) {
|
|
||||||
if (dbTask.getExecuteTime() == null) {
|
|
||||||
log.warn("跳过executeTime为空的延迟任务: taskId={}", dbTask.getTaskId());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
PendingTaskInfo info = new PendingTaskInfo();
|
|
||||||
info.setTaskId(dbTask.getTaskId());
|
|
||||||
info.setType(dbTask.getType());
|
|
||||||
info.setRoundId(dbTask.getRoundId());
|
|
||||||
info.setGroupId(dbTask.getGroupId());
|
|
||||||
info.setDigit(dbTask.getDigit());
|
|
||||||
info.setExecuteTime(dbTask.getExecuteTime());
|
|
||||||
pendingTasks.put(info.getTaskId(), info);
|
|
||||||
recovered++;
|
|
||||||
if (!dbTask.getExecuteTime().isAfter(now)) {
|
|
||||||
expiredCount++;
|
|
||||||
log.warn("恢复已过期任务,启动后立即执行: taskId={}, type={}, groupId={}, executeTime={}",
|
|
||||||
dbTask.getTaskId(), dbTask.getType(), dbTask.getGroupId(), dbTask.getExecuteTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("延迟任务恢复完成,共恢复 {} 个待执行任务,其中 {} 个已过期将立即执行",
|
|
||||||
recovered, expiredCount);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("恢复延迟任务失败: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取配置值
|
* 获取配置值
|
||||||
*/
|
*/
|
||||||
@@ -308,28 +207,111 @@ public class GameJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查局超时(每10秒执行)
|
* 扫描下注超时局 —— 每 2 秒
|
||||||
|
* <p>条件:{@code status=0 (下注中) AND start_time + bet_window_seconds 秒 < NOW()}
|
||||||
|
* <p>说明:{@code end_bet_time} 在 {@code startGame} 时为 null(仅群主手动 {@code endBet} 时写入),
|
||||||
|
* 所以基准时间戳必须用 {@code start_time},与原 {@code recoverRoundTasks} 的 fallback 逻辑一致。
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedDelay = 10000)
|
@Scheduled(fixedDelay = 2000)
|
||||||
public void checkRoundTimeout() {
|
public void scanRoundTimeouts() {
|
||||||
List<GameRound> ongoingRounds = gameRoundMapper.selectList(new LambdaQueryWrapper<GameRound>()
|
int betWindowSeconds = getConfigInt("bet_window_seconds", 60);
|
||||||
.eq(GameRound::getStatus, IGameService.STATUS_BETTING));
|
LocalDateTime threshold = LocalDateTime.now().minusSeconds(betWindowSeconds);
|
||||||
|
List<GameRound> overdue = gameRoundMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<GameRound>()
|
||||||
|
.eq(GameRound::getStatus, IGameService.STATUS_BETTING)
|
||||||
|
.isNotNull(GameRound::getStartTime)
|
||||||
|
.lt(GameRound::getStartTime, threshold)
|
||||||
|
.last("LIMIT 50"));
|
||||||
|
|
||||||
for (GameRound round : ongoingRounds) {
|
for (GameRound round : overdue) {
|
||||||
// 检查是否超过接注时间
|
|
||||||
RoundTask task = roundTasks.get(round.getId());
|
|
||||||
if (task != null && task.getEndBetTime().isBefore(LocalDateTime.now())) {
|
|
||||||
// 自动结束接注
|
|
||||||
try {
|
try {
|
||||||
gameService.endBet(round.getGroupId(), 0L); // 0L 表示系统自动触发
|
gameService.endBet(round.getGroupId(), 0L); // 0L 表示系统自动触发
|
||||||
// 触发开始发红包,延迟时间从配置读取
|
// 触发结束接注后的广播和发红包流程
|
||||||
int sendRpDelay = getConfigInt("send_rp_delay", 20);
|
onEndBet(round.getId(), round.getGroupId());
|
||||||
scheduleStartRp(round.getId(), round.getGroupId(), sendRpDelay);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("自动结束接注失败: roundId={}, error={}", round.getId(), e.getMessage());
|
log.error("自动结束接注失败: roundId={}, error={}", round.getId(), e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描"封盘后到点开始发红包" —— 每 2 秒
|
||||||
|
* <p>条件:{@code status=1 (已封盘) AND start_rp_time IS NULL AND end_bet_time + send_rp_delay 秒 < NOW()}
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 2000)
|
||||||
|
public void scanStartRpTimeouts() {
|
||||||
|
int sendRpDelay = getConfigInt("send_rp_delay", 20);
|
||||||
|
LocalDateTime threshold = LocalDateTime.now().minusSeconds(sendRpDelay);
|
||||||
|
List<GameRound> due = gameRoundMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<GameRound>()
|
||||||
|
.eq(GameRound::getStatus, IGameService.STATUS_BET_CLOSED)
|
||||||
|
.isNull(GameRound::getStartRpTime)
|
||||||
|
.lt(GameRound::getEndBetTime, threshold)
|
||||||
|
.last("LIMIT 50"));
|
||||||
|
|
||||||
|
for (GameRound round : due) {
|
||||||
|
try {
|
||||||
|
onStartRp(round.getId(), round.getGroupId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("延迟开始发红包失败: roundId={}, error={}", round.getId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描"发包超时由机器人代发" —— 每 2 秒
|
||||||
|
* <p>条件:{@code status=2 (发红包中) AND first_rp_id IS NULL AND start_rp_time + auto_rp_timeout 秒 < NOW()}
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 2000)
|
||||||
|
public void scanBotSendRpTimeouts() {
|
||||||
|
int autoRpTimeout = getConfigInt("auto_rp_timeout", 10);
|
||||||
|
LocalDateTime threshold = LocalDateTime.now().minusSeconds(autoRpTimeout);
|
||||||
|
List<GameRound> due = gameRoundMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<GameRound>()
|
||||||
|
.eq(GameRound::getStatus, IGameService.STATUS_RP_SENDING)
|
||||||
|
.isNull(GameRound::getFirstRpId)
|
||||||
|
.isNotNull(GameRound::getStartRpTime)
|
||||||
|
.lt(GameRound::getStartRpTime, threshold)
|
||||||
|
.last("LIMIT 50"));
|
||||||
|
|
||||||
|
for (GameRound round : due) {
|
||||||
|
try {
|
||||||
|
botSendRp(round.getId(), round.getGroupId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("机器人自动发包扫描执行失败: roundId={}, error={}", round.getId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描"结算后自动开下一期" —— 每 2 秒
|
||||||
|
* <p>条件:{@code status=3 (已结算) AND auto_next=1 AND finish_time + 5 秒 < NOW()}
|
||||||
|
* 且本群当前无进行中局
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 2000)
|
||||||
|
public void scanAutoNextTimeouts() {
|
||||||
|
int autoNextDelay = getConfigInt("auto_next_delay", 5);
|
||||||
|
LocalDateTime threshold = LocalDateTime.now().minusSeconds(autoNextDelay);
|
||||||
|
List<GameRound> finished = gameRoundMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<GameRound>()
|
||||||
|
.eq(GameRound::getStatus, IGameService.STATUS_FINISHED)
|
||||||
|
.eq(GameRound::getAutoNext, 1)
|
||||||
|
.isNotNull(GameRound::getFinishTime)
|
||||||
|
.lt(GameRound::getFinishTime, threshold)
|
||||||
|
.last("LIMIT 50"));
|
||||||
|
|
||||||
|
for (GameRound round : finished) {
|
||||||
|
try {
|
||||||
|
// 仅当该群当前无进行中局时才开下一期
|
||||||
|
GameRound ongoing = gameService.getOngoingRound(round.getGroupId());
|
||||||
|
if (ongoing != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
onAutoNext(round.getGroupId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("自动开下一期失败: groupId={}, error={}", round.getGroupId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -339,224 +321,33 @@ public class GameJob {
|
|||||||
public void resetDailyFakeUsers() {
|
public void resetDailyFakeUsers() {
|
||||||
fakeUserService.resetDailyActiveFakes();
|
fakeUserService.resetDailyActiveFakes();
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* 处理延迟任务队列(每秒执行)
|
|
||||||
*/
|
|
||||||
@Scheduled(fixedDelay = 1000)
|
|
||||||
public void processPendingTasks() {
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
|
||||||
for (Map.Entry<String, PendingTaskInfo> entry : pendingTasks.entrySet()) {
|
|
||||||
PendingTaskInfo task = entry.getValue();
|
|
||||||
if (task.getExecuteTime() == null) {
|
|
||||||
pendingTasks.remove(entry.getKey());
|
|
||||||
deleteTaskFromDb(task.getTaskId());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (task.getExecuteTime().isBefore(now) || task.getExecuteTime().isEqual(now)) {
|
|
||||||
boolean success = executeTask(task);
|
|
||||||
if (success) {
|
|
||||||
pendingTasks.remove(entry.getKey());
|
|
||||||
deleteTaskFromDb(task.getTaskId());
|
|
||||||
} else {
|
|
||||||
// 失败时保留内存与数据库中的任务,5秒后再重试
|
|
||||||
task.setExecuteTime(LocalDateTime.now().plusSeconds(5));
|
|
||||||
updateTaskExecuteTimeInDb(task);
|
|
||||||
log.warn("任务执行失败,保留任务待重试: taskId={}", task.getTaskId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新延迟任务的执行时间(失败重试场景)
|
* 开始游戏后广播并打日志
|
||||||
*/
|
|
||||||
private void updateTaskExecuteTimeInDb(PendingTaskInfo task) {
|
|
||||||
try {
|
|
||||||
PendingTask dbTask = pendingTaskMapper.selectOne(
|
|
||||||
new LambdaQueryWrapper<PendingTask>().eq(PendingTask::getTaskId, task.getTaskId()));
|
|
||||||
if (dbTask != null) {
|
|
||||||
dbTask.setExecuteTime(task.getExecuteTime());
|
|
||||||
pendingTaskMapper.updateById(dbTask);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("更新延迟任务执行时间失败: taskId={}, error={}", task.getTaskId(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行延迟任务
|
|
||||||
*
|
|
||||||
* @return true 表示执行成功(可清理);false 表示执行失败(需保留以重试)
|
|
||||||
*/
|
|
||||||
private boolean executeTask(PendingTaskInfo task) {
|
|
||||||
try {
|
|
||||||
switch (task.getType()) {
|
|
||||||
case "settle":
|
|
||||||
return onSettle(task.getRoundId(), task.getGroupId(), task.getDigit());
|
|
||||||
case "auto_next":
|
|
||||||
return onAutoNext(task.getGroupId());
|
|
||||||
case "start_rp":
|
|
||||||
return onStartRp(task.getRoundId(), task.getGroupId());
|
|
||||||
case "bot_send_rp":
|
|
||||||
return botSendRp(task.getRoundId(), task.getGroupId());
|
|
||||||
default:
|
|
||||||
log.warn("未知任务类型: {}", task.getType());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("执行任务失败: taskId={}, error={}", task.getTaskId(), e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 安排开始发红包
|
|
||||||
*/
|
|
||||||
public String scheduleStartRp(Long roundId, Long groupId, int delaySeconds) {
|
|
||||||
String taskId = "start_rp_" + groupId + "_" + taskIdGenerator.getAndIncrement();
|
|
||||||
PendingTaskInfo task = new PendingTaskInfo();
|
|
||||||
task.setTaskId(taskId);
|
|
||||||
task.setType("start_rp");
|
|
||||||
task.setRoundId(roundId);
|
|
||||||
task.setGroupId(groupId);
|
|
||||||
LocalDateTime executeTime = LocalDateTime.now().plusSeconds(delaySeconds);
|
|
||||||
task.setExecuteTime(executeTime);
|
|
||||||
pendingTasks.put(taskId, task);
|
|
||||||
// 持久化到数据库
|
|
||||||
saveTaskToDb(taskId, "start_rp", roundId, groupId, null, executeTime);
|
|
||||||
log.info("安排{}秒后开始发红包: roundId={}", delaySeconds, roundId);
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安排自动结算
|
|
||||||
*/
|
|
||||||
public String scheduleSettle(Long roundId, Long groupId, int digit, int delaySeconds) {
|
|
||||||
String taskId = "settle_" + groupId + "_" + taskIdGenerator.getAndIncrement();
|
|
||||||
PendingTaskInfo task = new PendingTaskInfo();
|
|
||||||
task.setTaskId(taskId);
|
|
||||||
task.setType("settle");
|
|
||||||
task.setRoundId(roundId);
|
|
||||||
task.setGroupId(groupId);
|
|
||||||
task.setDigit(digit);
|
|
||||||
LocalDateTime executeTime = LocalDateTime.now().plusSeconds(delaySeconds);
|
|
||||||
task.setExecuteTime(executeTime);
|
|
||||||
pendingTasks.put(taskId, task);
|
|
||||||
// 持久化到数据库
|
|
||||||
saveTaskToDb(taskId, "settle", roundId, groupId, digit, executeTime);
|
|
||||||
log.info("安排{}秒后结算: taskId={}, roundId={}, digit={}", delaySeconds, taskId, roundId, digit);
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安排自动下一期
|
|
||||||
*/
|
|
||||||
public String scheduleAutoNextTask(Long groupId, int delaySeconds) {
|
|
||||||
String taskId = "auto_next_" + groupId + "_" + taskIdGenerator.getAndIncrement();
|
|
||||||
PendingTaskInfo task = new PendingTaskInfo();
|
|
||||||
task.setTaskId(taskId);
|
|
||||||
task.setType("auto_next");
|
|
||||||
task.setGroupId(groupId);
|
|
||||||
LocalDateTime executeTime = LocalDateTime.now().plusSeconds(delaySeconds);
|
|
||||||
task.setExecuteTime(executeTime);
|
|
||||||
pendingTasks.put(taskId, task);
|
|
||||||
// 持久化到数据库
|
|
||||||
saveTaskToDb(taskId, "auto_next", null, groupId, null, executeTime);
|
|
||||||
log.info("安排{}秒后自动开始下一期: groupId={}", delaySeconds, groupId);
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安排机器人发包
|
|
||||||
*/
|
|
||||||
public String scheduleBotSendRp(Long roundId, Long groupId, int delaySeconds) {
|
|
||||||
String taskId = "bot_send_rp_" + groupId + "_" + taskIdGenerator.getAndIncrement();
|
|
||||||
PendingTaskInfo task = new PendingTaskInfo();
|
|
||||||
task.setTaskId(taskId);
|
|
||||||
task.setType("bot_send_rp");
|
|
||||||
task.setRoundId(roundId);
|
|
||||||
task.setGroupId(groupId);
|
|
||||||
LocalDateTime executeTime = LocalDateTime.now().plusSeconds(delaySeconds);
|
|
||||||
task.setExecuteTime(executeTime);
|
|
||||||
pendingTasks.put(taskId, task);
|
|
||||||
// 持久化到数据库
|
|
||||||
saveTaskToDb(taskId, "bot_send_rp", roundId, groupId, null, executeTime);
|
|
||||||
log.info("安排{}秒后机器人发包: roundId={}", delaySeconds, roundId);
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存任务到数据库
|
|
||||||
*/
|
|
||||||
private void saveTaskToDb(String taskId, String type, Long roundId, Long groupId, Integer digit, LocalDateTime executeTime) {
|
|
||||||
try {
|
|
||||||
PendingTask dbTask = new PendingTask();
|
|
||||||
dbTask.setTaskId(taskId);
|
|
||||||
dbTask.setType(type);
|
|
||||||
dbTask.setRoundId(roundId);
|
|
||||||
dbTask.setGroupId(groupId);
|
|
||||||
dbTask.setDigit(digit);
|
|
||||||
dbTask.setExecuteTime(executeTime);
|
|
||||||
dbTask.setCreateTime(LocalDateTime.now());
|
|
||||||
pendingTaskMapper.insert(dbTask);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("保存任务到数据库失败: taskId={}, error={}", taskId, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从数据库删除任务
|
|
||||||
*/
|
|
||||||
private void deleteTaskFromDb(String taskId) {
|
|
||||||
try {
|
|
||||||
pendingTaskMapper.delete(new LambdaQueryWrapper<PendingTask>()
|
|
||||||
.eq(PendingTask::getTaskId, taskId));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("从数据库删除任务失败: taskId={}, error={}", taskId, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开始游戏后安排定时器
|
|
||||||
*/
|
*/
|
||||||
public void onGameStart(Long roundId, Long groupId, int intervalSeconds) {
|
public void onGameStart(Long roundId, Long groupId, int intervalSeconds) {
|
||||||
RoundTask task = new RoundTask();
|
|
||||||
task.setRoundId(roundId);
|
|
||||||
task.setGroupId(groupId);
|
|
||||||
task.setEndBetTime(LocalDateTime.now().plusSeconds(intervalSeconds));
|
|
||||||
roundTasks.put(roundId, task);
|
|
||||||
|
|
||||||
// 广播开始下注
|
|
||||||
GameRound round = gameRoundMapper.selectById(roundId);
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
if (round != null) {
|
if (round != null) {
|
||||||
broadcastStartBet(groupId, round.getRoundNo());
|
broadcaster.broadcastStartBet(groupId, round.getRoundNo());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("游戏开始: roundId={}, {}秒后结束接注", roundId, intervalSeconds);
|
log.info("游戏开始: roundId={}, {}秒后结束接注", roundId, intervalSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 结束接注后安排发红包
|
* 结束接注后广播 + 通知白名单
|
||||||
|
* <p>延迟开始发红包由 {@link #scanStartRpTimeouts()} 接管
|
||||||
*/
|
*/
|
||||||
public void onEndBet(Long roundId, Long groupId) {
|
public void onEndBet(Long roundId, Long groupId) {
|
||||||
// 移除结束接注定时器
|
|
||||||
roundTasks.remove(roundId);
|
|
||||||
|
|
||||||
// 广播结束下注
|
|
||||||
GameRound round = gameRoundMapper.selectById(roundId);
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
if (round != null) {
|
if (round != null) {
|
||||||
broadcastEndBet(groupId, round.getRoundNo());
|
broadcaster.broadcastEndBet(groupId, round.getRoundNo());
|
||||||
}
|
}
|
||||||
|
broadcaster.notifyWhiteUsers(roundId);
|
||||||
// 通知白名单用户尾数试算完成
|
|
||||||
notifyWhiteUsers(roundId);
|
|
||||||
|
|
||||||
// 安排延迟后开始发红包
|
|
||||||
int sendRpDelay = getConfigInt("send_rp_delay", 20);
|
|
||||||
scheduleStartRp(roundId, groupId, sendRpDelay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始发红包
|
* 开始发红包
|
||||||
|
* <p>机器人超时发包由 {@link #scanBotSendRpTimeouts()} 接管
|
||||||
*/
|
*/
|
||||||
public boolean onStartRp(Long roundId, Long groupId) {
|
public boolean onStartRp(Long roundId, Long groupId) {
|
||||||
try {
|
try {
|
||||||
@@ -565,20 +356,23 @@ public class GameJob {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 再次校验:避免重复推进状态机
|
||||||
|
if (round.getStatus() == null || round.getStatus() != IGameService.STATUS_BET_CLOSED) {
|
||||||
|
log.debug("本局{}当前状态非封盘中(status={}),跳过发红包开始",
|
||||||
|
roundId, round.getStatus());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 更新局状态为开始发红包
|
// 更新局状态为开始发红包
|
||||||
round.setStatus(IGameService.STATUS_RP_SENDING);
|
round.setStatus(IGameService.STATUS_RP_SENDING);
|
||||||
round.setStartRpTime(LocalDateTime.now());
|
round.setStartRpTime(LocalDateTime.now());
|
||||||
gameRoundMapper.updateById(round);
|
gameRoundMapper.updateById(round);
|
||||||
|
|
||||||
// 广播开始发红包
|
// 广播开始发红包
|
||||||
broadcastStartRp(groupId, round.getRoundNo());
|
broadcaster.broadcastStartRp(groupId, round.getRoundNo());
|
||||||
|
|
||||||
// 安排超时后如果没人发包则机器人自动发
|
|
||||||
int autoRpTimeout = getConfigInt("auto_rp_timeout", 10);
|
|
||||||
scheduleBotSendRp(roundId, groupId, autoRpTimeout);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("开始发红包失败: roundId={}, error={}", roundId, e.getMessage());
|
log.error("开始发红包失败: roundId={}, error={}", roundId, e.getMessage(), e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -649,6 +443,7 @@ public class GameJob {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 开奖结算
|
* 开奖结算
|
||||||
|
* <p>自动开下一期由 {@link #scanAutoNextTimeouts()} 接管
|
||||||
*/
|
*/
|
||||||
public boolean onSettle(Long roundId, Long groupId, int digit) {
|
public boolean onSettle(Long roundId, Long groupId, int digit) {
|
||||||
try {
|
try {
|
||||||
@@ -657,139 +452,17 @@ public class GameJob {
|
|||||||
// 广播开奖结果
|
// 广播开奖结果
|
||||||
GameRound round = gameRoundMapper.selectById(roundId);
|
GameRound round = gameRoundMapper.selectById(roundId);
|
||||||
if (round != null) {
|
if (round != null) {
|
||||||
broadcastResult(groupId, digit, round.getRoundNo());
|
broadcaster.broadcastResult(groupId, digit, round.getRoundNo());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否自动开启下一期
|
|
||||||
GameRound roundAfterSettle = gameRoundMapper.selectById(roundId);
|
|
||||||
if (roundAfterSettle != null && roundAfterSettle.getAutoNext() == 1) {
|
|
||||||
// 5秒后自动开始下一期
|
|
||||||
scheduleAutoNextTask(groupId, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理任务
|
|
||||||
roundTasks.remove(roundId);
|
|
||||||
|
|
||||||
log.info("局结算完成: roundId={}, digit={}", roundId, digit);
|
log.info("局结算完成: roundId={}, digit={}", roundId, digit);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("结算失败: roundId={}, error={}", roundId, e.getMessage());
|
log.error("结算失败: roundId={}, error={}", roundId, e.getMessage(), e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 通知白名单用户(尾数试算完成)
|
|
||||||
*/
|
|
||||||
private void notifyWhiteUsers(Long roundId) {
|
|
||||||
List<DigitWhite> whites = digitWhiteMapper.selectList(new LambdaQueryWrapper<>());
|
|
||||||
GameRound round = gameRoundMapper.selectById(roundId);
|
|
||||||
if (round == null) return;
|
|
||||||
|
|
||||||
for (DigitWhite white : whites) {
|
|
||||||
User user = userMapper.selectById(white.getUserId());
|
|
||||||
if (user != null) {
|
|
||||||
// 通过OpenIM发送通知
|
|
||||||
openIMApiClient.sendMsg(
|
|
||||||
openIMProperties.getAdminUserId(),
|
|
||||||
String.valueOf(user.getId()),
|
|
||||||
1, // sessionType 1=单聊
|
|
||||||
101, // contentType 101=文本
|
|
||||||
java.util.Map.of("content", "尾数试算完成,请前往修改尾数。期号: " + round.getRoundNo())
|
|
||||||
);
|
|
||||||
log.info("通知白名单用户尾数试算完成: userId={}, roundId={}", user.getId(), roundId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播开始下注
|
|
||||||
*/
|
|
||||||
private void broadcastStartBet(Long groupId, String roundNo) {
|
|
||||||
// 注意: 实际生产中需要通过Group表映射获取OpenIM群ID
|
|
||||||
openIMApiClient.sendMsg(
|
|
||||||
openIMProperties.getAdminUserId(),
|
|
||||||
String.valueOf(groupId),
|
|
||||||
3, // sessionType 3=群聊
|
|
||||||
101, // contentType 101=文本
|
|
||||||
java.util.Map.of("content", roundNo + "期开始下注")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播结束下注
|
|
||||||
*/
|
|
||||||
private void broadcastEndBet(Long groupId, String roundNo) {
|
|
||||||
openIMApiClient.sendMsg(
|
|
||||||
openIMProperties.getAdminUserId(),
|
|
||||||
String.valueOf(groupId),
|
|
||||||
3, // sessionType 3=群聊
|
|
||||||
101, // contentType 101=文本
|
|
||||||
java.util.Map.of("content", roundNo + "期结束下注")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播开始发红包
|
|
||||||
*/
|
|
||||||
private void broadcastStartRp(Long groupId, String roundNo) {
|
|
||||||
openIMApiClient.sendMsg(
|
|
||||||
openIMProperties.getAdminUserId(),
|
|
||||||
String.valueOf(groupId),
|
|
||||||
3, // sessionType 3=群聊
|
|
||||||
101, // contentType 101=文本
|
|
||||||
java.util.Map.of("content", roundNo + "期开始发红包")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播开奖结果
|
|
||||||
*/
|
|
||||||
private void broadcastResult(Long groupId, int digit, String roundNo) {
|
|
||||||
String playWinDesc = gameService.getPlayWinDesc(digit);
|
|
||||||
String message = roundNo + "期开奖\n" +
|
|
||||||
"尾数: " + digit + "\n" +
|
|
||||||
"中奖玩法: " + playWinDesc;
|
|
||||||
|
|
||||||
// 查询近30期历史
|
|
||||||
String history = getRecentHistory(groupId);
|
|
||||||
if (history != null && !history.isEmpty()) {
|
|
||||||
message += "\n\n近30期:\n" + history;
|
|
||||||
}
|
|
||||||
|
|
||||||
openIMApiClient.sendMsg(
|
|
||||||
openIMProperties.getAdminUserId(),
|
|
||||||
String.valueOf(groupId),
|
|
||||||
3, // sessionType 3=群聊
|
|
||||||
101, // contentType 101=文本
|
|
||||||
java.util.Map.of("content", message)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取近30期历史开奖记录
|
|
||||||
*/
|
|
||||||
private String getRecentHistory(Long groupId) {
|
|
||||||
List<DrawRecord> records = drawRecordMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<DrawRecord>()
|
|
||||||
.eq(DrawRecord::getGroupId, groupId)
|
|
||||||
.orderByDesc(DrawRecord::getCreateTime)
|
|
||||||
.last("LIMIT 30"));
|
|
||||||
|
|
||||||
if (records == null || records.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (DrawRecord record : records) {
|
|
||||||
sb.append(record.getRoundNo()).append(":").append(record.getDigit()).append("\n");
|
|
||||||
}
|
|
||||||
return sb.toString().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自动开始下一期
|
* 自动开始下一期
|
||||||
*/
|
*/
|
||||||
@@ -800,12 +473,12 @@ public class GameJob {
|
|||||||
log.warn("自动开始下一期失败:startGame未返回局信息, groupId={}", groupId);
|
log.warn("自动开始下一期失败:startGame未返回局信息, groupId={}", groupId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 复用与手动流程一致的逻辑:注册roundTasks监控 + 广播开始下注
|
// 复用与手动流程一致的逻辑:广播开始下注
|
||||||
int betWindowSeconds = getConfigInt("bet_window_seconds", 60);
|
int betWindowSeconds = getConfigInt("bet_window_seconds", 60);
|
||||||
onGameStart(round.getId(), groupId, betWindowSeconds);
|
onGameStart(round.getId(), groupId, betWindowSeconds);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("自动开始下一期失败: groupId={}, error={}", groupId, e.getMessage());
|
log.error("自动开始下一期失败: groupId={}, error={}", groupId, e.getMessage(), e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
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> {
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.tailbet.mapper;
|
|
||||||
|
|
||||||
import com.tailbet.model.entity.PendingTask;
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 延迟任务Mapper
|
|
||||||
*/
|
|
||||||
@Mapper
|
|
||||||
public interface PendingTaskMapper extends BaseMapper<PendingTask> {
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.tailbet.model;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 延迟任务信息(内存缓存用)
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class PendingTaskInfo {
|
|
||||||
private String taskId;
|
|
||||||
private String type;
|
|
||||||
private Long roundId;
|
|
||||||
private Long groupId;
|
|
||||||
private Integer digit;
|
|
||||||
private LocalDateTime executeTime;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.tailbet.model;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 局任务信息
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class RoundTask {
|
|
||||||
private Long roundId;
|
|
||||||
private Long groupId;
|
|
||||||
private LocalDateTime endBetTime;
|
|
||||||
private LocalDateTime startRpTime;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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("pending_task")
|
|
||||||
public class PendingTask {
|
|
||||||
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务ID
|
|
||||||
*/
|
|
||||||
private String taskId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务类型: start_rp/settle/auto_next/bot_send_rp
|
|
||||||
*/
|
|
||||||
private String type;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 局ID
|
|
||||||
*/
|
|
||||||
private Long roundId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 群ID
|
|
||||||
*/
|
|
||||||
private Long groupId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开奖尾数(settle任务用)
|
|
||||||
*/
|
|
||||||
private Integer digit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行时间
|
|
||||||
*/
|
|
||||||
private LocalDateTime executeTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建时间
|
|
||||||
*/
|
|
||||||
private LocalDateTime createTime;
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,8 @@ package com.tailbet.service.impl;
|
|||||||
|
|
||||||
|
|
||||||
import com.tailbet.mapper.UserMapper;
|
import com.tailbet.mapper.UserMapper;
|
||||||
import com.tailbet.mapper.DigitWhiteMapper;
|
import com.tailbet.mapper.GroupMemberMapper;
|
||||||
import com.tailbet.model.entity.DigitWhite;
|
import com.tailbet.model.entity.GroupMember;
|
||||||
import com.tailbet.model.entity.User;
|
import com.tailbet.model.entity.User;
|
||||||
import com.tailbet.openim.OpenIMApiClient;
|
import com.tailbet.openim.OpenIMApiClient;
|
||||||
import com.tailbet.service.IPushService;
|
import com.tailbet.service.IPushService;
|
||||||
@@ -13,6 +13,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 推送服务实现
|
* 推送服务实现
|
||||||
@@ -23,30 +25,76 @@ import java.util.List;
|
|||||||
public class PushServiceImpl implements IPushService {
|
public class PushServiceImpl implements IPushService {
|
||||||
|
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
private final DigitWhiteMapper digitWhiteMapper;
|
private final GroupMemberMapper groupMemberMapper;
|
||||||
private final OpenIMApiClient openIMApiClient;
|
private final OpenIMApiClient openIMApiClient;
|
||||||
private final com.tailbet.config.OpenIMProperties openIMProperties;
|
private final com.tailbet.config.OpenIMProperties openIMProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 推送尾数试算完成通知给白名单用户
|
* 推送尾数试算完成通知给当前群的白名单用户
|
||||||
|
* <p>白名单以 {@code user.is_white=1} 为权威源,与本群成员({@code group_member})取交集后推送。
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void pushDigitTrialComplete(Long groupId, Long roundId) {
|
public void pushDigitTrialComplete(Long groupId, Long roundId) {
|
||||||
List<DigitWhite> whites = digitWhiteMapper.selectList(new LambdaQueryWrapper<DigitWhite>());
|
// 1. 取本群成员用户ID集合
|
||||||
for (DigitWhite white : whites) {
|
Set<Long> memberIds = groupMemberMapper.selectList(
|
||||||
User user = userMapper.selectById(white.getUserId());
|
new LambdaQueryWrapper<GroupMember>()
|
||||||
if (user != null) {
|
.eq(GroupMember::getGroupId, groupId)
|
||||||
IPushService.PushMessage message = new IPushService.PushMessage();
|
.select(GroupMember::getUserId))
|
||||||
message.setType(PUSH_TYPE_DIGIT_TRIAL);
|
.stream()
|
||||||
message.setTitle("尾数试算完成");
|
.map(GroupMember::getUserId)
|
||||||
message.setContent("可前往修改尾数");
|
.collect(Collectors.toSet());
|
||||||
message.setUserId(user.getId());
|
if (memberIds.isEmpty()) {
|
||||||
message.setGroupId(groupId);
|
log.debug("群{}无成员,跳过白名单通知", groupId);
|
||||||
message.setRoundId(roundId);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 取白名单用户ID集合(user.is_white = 1)
|
||||||
|
Set<Long> whiteIds = userMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<User>()
|
||||||
|
.eq(User::getIsWhite, 1)
|
||||||
|
.select(User::getId))
|
||||||
|
.stream()
|
||||||
|
.map(User::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (whiteIds.isEmpty()) {
|
||||||
|
log.debug("无白名单用户,跳过通知: groupId={}", groupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 交集:本群成员 ∩ 白名单
|
||||||
|
memberIds.retainAll(whiteIds);
|
||||||
|
if (memberIds.isEmpty()) {
|
||||||
|
log.debug("群{}无白名单成员,跳过通知", groupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPushService.PushMessage template = new IPushService.PushMessage();
|
||||||
|
template.setType(PUSH_TYPE_DIGIT_TRIAL);
|
||||||
|
template.setTitle("尾数试算完成");
|
||||||
|
template.setContent("可前往修改尾数");
|
||||||
|
template.setGroupId(groupId);
|
||||||
|
template.setRoundId(roundId);
|
||||||
|
|
||||||
|
for (Long userId : memberIds) {
|
||||||
|
IPushService.PushMessage message = cloneMessage(template);
|
||||||
|
message.setUserId(userId);
|
||||||
sendPush(message);
|
sendPush(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 克隆推送消息模板,避免多个用户共享同一对象
|
||||||
|
*/
|
||||||
|
private IPushService.PushMessage cloneMessage(IPushService.PushMessage src) {
|
||||||
|
IPushService.PushMessage dst = new IPushService.PushMessage();
|
||||||
|
dst.setType(src.getType());
|
||||||
|
dst.setTitle(src.getTitle());
|
||||||
|
dst.setContent(src.getContent());
|
||||||
|
dst.setGroupId(src.getGroupId());
|
||||||
|
dst.setRoundId(src.getRoundId());
|
||||||
|
dst.setRoundNo(src.getRoundNo());
|
||||||
|
dst.setDigit(src.getDigit());
|
||||||
|
return dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ mybatis-plus:
|
|||||||
type-aliases-package: com.tailbet.entity
|
type-aliases-package: com.tailbet.entity
|
||||||
configuration:
|
configuration:
|
||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
# log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||||
|
|
||||||
global-config:
|
global-config:
|
||||||
db-config:
|
db-config:
|
||||||
@@ -64,9 +64,9 @@ openim:
|
|||||||
|
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
secret: ${JWT_SECRET:4d19a5c2984044848df9bed721512f9f}
|
secret: ${JWT_SECRET:ecc6de473d6f4e0fbdaf5ea899fd2e26}
|
||||||
expiration: 604800000
|
expiration: 604800000
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.tailbet: debug
|
com.tailbet: info
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ CREATE TABLE IF NOT EXISTS game_round (
|
|||||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_group_round (group_id, round_no),
|
UNIQUE KEY uk_group_round (group_id, round_no),
|
||||||
INDEX idx_status (status),
|
INDEX idx_status (status),
|
||||||
INDEX idx_group_id (group_id)
|
INDEX idx_group_id (group_id),
|
||||||
|
INDEX idx_status_endbet (status, end_bet_time),
|
||||||
|
INDEX idx_status_startrp (status, start_rp_time),
|
||||||
|
INDEX idx_status_finishtime (status, finish_time)
|
||||||
) COMMENT '游戏局表';
|
) COMMENT '游戏局表';
|
||||||
|
|
||||||
-- 注单表
|
-- 注单表
|
||||||
@@ -165,14 +168,16 @@ CREATE TABLE IF NOT EXISTS draw_record (
|
|||||||
INDEX idx_group_time (group_id, create_time)
|
INDEX idx_group_time (group_id, create_time)
|
||||||
) COMMENT '开奖记录表';
|
) COMMENT '开奖记录表';
|
||||||
|
|
||||||
-- 尾数控制白名单表
|
-- 尾数控制白名单表(已废弃:白名单以 user.is_white 字段为准,digit_white 表不再被应用写入)
|
||||||
CREATE TABLE IF NOT EXISTS digit_white (
|
-- 如需彻底清理可执行:
|
||||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
-- DROP TABLE IF EXISTS `digit_white`;
|
||||||
user_id BIGINT NOT NULL UNIQUE COMMENT '用户ID',
|
-- CREATE TABLE IF NOT EXISTS digit_white (
|
||||||
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
-- id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
-- user_id BIGINT NOT NULL UNIQUE COMMENT '用户ID',
|
||||||
INDEX idx_user (user_id)
|
-- deleted TINYINT DEFAULT 0 COMMENT '逻辑删除',
|
||||||
) COMMENT '尾数控制白名单';
|
-- create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
-- INDEX idx_user (user_id)
|
||||||
|
-- ) COMMENT '尾数控制白名单';
|
||||||
|
|
||||||
-- 审计日志表
|
-- 审计日志表
|
||||||
CREATE TABLE IF NOT EXISTS audit_log (
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
@@ -238,18 +243,21 @@ INSERT INTO sys_config (cfg_key, cfg_value, remark) VALUES
|
|||||||
('bet_window_seconds', '60', '接注窗口时间(秒)'),
|
('bet_window_seconds', '60', '接注窗口时间(秒)'),
|
||||||
('fake_recv_seconds', '5', '托领取超时时间(秒)');
|
('fake_recv_seconds', '5', '托领取超时时间(秒)');
|
||||||
|
|
||||||
CREATE TABLE `pending_task` (
|
-- ====== 去内存 Map 重构后,pending_task 表已不再被应用写入,保留 DDL 仅作历史兼容参考 ======
|
||||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
|
-- 实际生产如需清理,可执行:
|
||||||
`task_id` VARCHAR(64) NOT NULL COMMENT '任务ID',
|
-- DROP TABLE IF EXISTS `pending_task`;
|
||||||
`type` VARCHAR(32) NOT NULL COMMENT 'start_rp/settle/auto_next/bot_send_rp',
|
-- CREATE TABLE `pending_task` (
|
||||||
`round_id` BIGINT COMMENT '局ID',
|
-- `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
`group_id` BIGINT NOT NULL COMMENT '群ID',
|
-- `task_id` VARCHAR(64) NOT NULL COMMENT '任务ID',
|
||||||
`digit` INT COMMENT '开奖尾数(settle任务用)',
|
-- `type` VARCHAR(32) NOT NULL COMMENT 'start_rp/settle/auto_next/bot_send_rp',
|
||||||
`execute_time` DATETIME NOT NULL COMMENT '执行时间',
|
-- `round_id` BIGINT COMMENT '局ID',
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
-- `group_id` BIGINT NOT NULL COMMENT '群ID',
|
||||||
INDEX `idx_execute_time` (`execute_time`),
|
-- `digit` INT COMMENT '开奖尾数(settle任务用)',
|
||||||
UNIQUE INDEX `idx_task_id` (`task_id`)
|
-- `execute_time` DATETIME NOT NULL COMMENT '执行时间',
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
-- `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
-- INDEX `idx_execute_time` (`execute_time`),
|
||||||
|
-- UNIQUE INDEX `idx_task_id` (`task_id`)
|
||||||
|
-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ====== 积分字段类型迁移:BIGINT → DECIMAL(20,2),与红包金额统一精度 ======
|
-- ====== 积分字段类型迁移:BIGINT → DECIMAL(20,2),与红包金额统一精度 ======
|
||||||
-- 历史BIGINT都是整数,DECIMAL(20,2)可无损容纳,无需特殊转换
|
-- 历史BIGINT都是整数,DECIMAL(20,2)可无损容纳,无需特殊转换
|
||||||
|
|||||||
Reference in New Issue
Block a user