This commit is contained in:
wells
2026-08-05 13:48:48 +08:00
commit 1303bf37dc
80 changed files with 5154 additions and 0 deletions
@@ -0,0 +1,21 @@
package com.tailbet.service;
import com.tailbet.model.entity.AuditLog;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* 审计日志Service接口
*/
public interface IAuditService {
/**
* 记录审计日志
*/
void log(Long groupId, Long roundId, Long userId, String action, String detail);
/**
* 分页查询审计日志
*/
Page<AuditLog> getPage(Long groupId, Long roundId, String action,
Integer pageNum, Integer pageSize);
}
@@ -0,0 +1,27 @@
package com.tailbet.service;
import com.tailbet.model.entity.BetOrder;
import java.math.BigDecimal;
/**
* 注单Service接口
*/
public interface IBetService {
/**
* 下注
*/
BetOrder placeBet(Long userId, Long groupId, String clientMsgId,
String playType, Integer betAmount);
/**
* 获取赔率
*/
BigDecimal getOdds(String playType);
/**
* 获取用户本局下注总额
*/
Integer getUserRoundBetAmount(Long userId, Long roundId);
}
@@ -0,0 +1,41 @@
package com.tailbet.service;
import com.tailbet.model.entity.User;
import java.util.List;
/**
* 假人服务接口
*/
public interface IFakeUserService {
/**
* 初始化群假人
*/
void initGroupFakeUsers(Long groupId, int minCount, int maxCount);
/**
* 创建单个假人
*/
User createFakeUser(Long groupId);
/**
* 每日重置活跃假人
*/
void resetDailyActiveFakes();
/**
* 检查用户是否是今日活跃假人
*/
boolean isTodayActiveFake(Long userId);
/**
* 假人下注
*/
void fakeBet(Long groupId);
/**
* 获取假人列表
*/
List<User> getFakeUsers();
}
@@ -0,0 +1,92 @@
package com.tailbet.service;
import com.tailbet.model.entity.GameRound;
import com.tailbet.model.entity.DrawRecord;
import com.tailbet.model.entity.BetOrder;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 游戏Service接口
*/
public interface IGameService {
// 局状态常量
int STATUS_ONGOING = 0;
int STATUS_ENDED = 1;
// 玩法赔率
BigDecimal ODDS_SINGLE_DOUBLE = new BigDecimal("1");
BigDecimal ODDS_SIZE = new BigDecimal("1");
BigDecimal ODDS_DIGIT = new BigDecimal("2");
/**
* 尾数试算结果
*/
@Data
class DigitTrial {
private int digit;
private String playWin;
private int totalBet;
private long totalAward;
private long profit;
}
/**
* 获取群当前进行中的局
*/
GameRound getOngoingRound(Long groupId);
/**
* 生成期号
*/
String generateRoundNo(Long groupId);
/**
* 开始游戏(群主操作)
*/
GameRound startGame(Long groupId, Long userId);
/**
* 结束接注
*/
GameRound endBet(Long groupId, Long userId);
/**
* 停止自动连开
*/
void stopAutoNext(Long groupId, Long userId);
/**
* 设置目标尾数
*/
void setTargetDigit(Long groupId, Integer digit, Long userId);
/**
* 尾数试算
*/
List<DigitTrial> trialDigit(Long groupId, Long roundId);
/**
* 判定是否中奖
*/
boolean isWin(BetOrder bet, int digit);
/**
* 获取中奖玩法描述
*/
String getPlayWinDesc(int digit);
/**
* 开奖结算
*/
void settle(Long roundId, int digit);
/**
* 获取开奖历史
*/
Page<DrawRecord> getHistory(Long groupId, Integer pageNum, Integer pageSize);
}
@@ -0,0 +1,62 @@
package com.tailbet.service;
import com.tailbet.model.entity.Group;
import com.tailbet.model.entity.GroupMember;
import java.util.List;
/**
* 群Service接口
*/
public interface IGroupService {
// 角色
int ROLE_NORMAL = 0;
int ROLE_ADMIN = 1;
int ROLE_OWNER = 2;
/**
* 获取群信息
*/
Group getById(Long groupId);
/**
* 获取用户的群列表
*/
List<Group> getUserGroups(Long userId);
/**
* 拉用户入群
*/
void pullUser(Long groupId, Long userId, Long operatorId);
/**
* 踢出用户
*/
void kickUser(Long groupId, Long targetUserId, Long operatorId);
/**
* 设置管理员
*/
void setAdmin(Long groupId, Long targetUserId, Long operatorId);
/**
* 取消管理员
*/
void removeAdmin(Long groupId, Long targetUserId, Long operatorId);
/**
* 获取群成员
*/
List<GroupMember> getMembers(Long groupId);
/**
* 检查用户是否是群主
*/
boolean isOwner(Long groupId, Long userId);
/**
* 检查用户是否是管理员
*/
boolean isAdmin(Long groupId, Long userId);
}
@@ -0,0 +1,38 @@
package com.tailbet.service;
import com.tailbet.model.entity.PointsFlow;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* 积分Service接口
*/
public interface IPointsService {
// 积分类型常量
String TYPE_OPS_ADD = "ops_add";
String TYPE_OPS_SUB = "ops_sub";
String TYPE_BET = "bet";
String TYPE_AWARD = "award";
String TYPE_RP_SEND = "rp_send";
String TYPE_RP_RECV = "rp_recv";
/**
* 添加积分
*/
void addPoints(Long userId, Long amount, String type, String bizNo, Long operatorId, String remark);
/**
* 扣除积分
*/
boolean subPoints(Long userId, Long amount, String type, String bizNo, Long operatorId, String remark);
/**
* 获取用户积分
*/
Long getPoints(Long userId);
/**
* 查询积分流水记录
*/
Page<PointsFlow> getFlowPage(Long userId, Integer pageNum, Integer pageSize);
}
@@ -0,0 +1,38 @@
package com.tailbet.service;
/**
* 推送服务接口
*/
public interface IPushService {
// 推送类型常量
String PUSH_TYPE_DIGIT_TRIAL = "digit_trial"; // 尾数试算完成
String PUSH_TYPE_GAME_START = "game_start"; // 游戏开始
String PUSH_TYPE_GAME_END = "game_end"; // 游戏结束
String PUSH_TYPE_RESULT = "result"; // 开奖结果
/**
* 推送尾数试算完成通知给白名单用户
*/
void pushDigitTrialComplete(Long groupId, Long roundId);
/**
* 推送游戏开始通知
*/
void pushGameStart(Long groupId, String roundNo);
/**
* 推送开奖结果通知
*/
void pushGameResult(Long groupId, String roundNo, int digit, String playWins);
/**
* 发送单用户推送
*/
void sendPush(PushService.PushMessage message);
/**
* 广播推送消息给群成员
*/
void broadcastToGroup(Long groupId, PushService.PushMessage message);
}
@@ -0,0 +1,59 @@
package com.tailbet.service;
import com.tailbet.model.entity.RedPacket;
import com.tailbet.model.entity.RedPacketRecv;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.math.BigDecimal;
import java.util.List;
/**
* 红包Service接口
*/
public interface IRedPacketService {
// 红包状态
int STATUS_ONGOING = 0;
int STATUS_FINISHED = 1;
int STATUS_REFUND = 2;
// 红包类型
String TYPE_GAME = "game";
String TYPE_RANDOM = "random";
/**
* 发红包
*/
RedPacket sendRp(Long userId, Long groupId, BigDecimal totalAmount,
Integer totalCount, Long roundId);
/**
* 领红包
*/
RedPacketRecv receiveRp(Long rpId, Long userId);
/**
* 计算领取金额
*/
BigDecimal calculateReceiveAmount(RedPacket rp);
/**
* 获取红包详情
*/
RedPacket getById(Long rpId);
/**
* 获取红包领取列表
*/
List<RedPacketRecv> getReceives(Long rpId);
/**
* 查询红包记录
*/
Page<RedPacket> getRecords(Long userId, Integer pageNum, Integer pageSize);
/**
* 查找进行中的游戏红包
*/
RedPacket getOngoingGameRp(Long groupId);
}
@@ -0,0 +1,30 @@
package com.tailbet.service;
import com.tailbet.model.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 用户Service接口
*/
public interface IUserService extends IService<User> {
/**
* 根据用户名查询用户
*/
User getByUsername(String username);
/**
* 注册用户
*/
User register(String username, String password);
/**
* 用户登录
*/
String login(String username, String password);
/**
* 获取当前登录用户
*/
User getCurrentUser();
}
@@ -0,0 +1,59 @@
package com.tailbet.service.impl;
import com.tailbet.model.entity.AuditLog;
import com.tailbet.mapper.AuditLogMapper;
import com.tailbet.service.IAuditService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* 审计日志Service实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class AuditServiceImpl implements IAuditService {
private final AuditLogMapper auditLogMapper;
/**
* 记录审计日志
*/
@Override
public void log(Long groupId, Long roundId, Long userId, String action, String detail) {
AuditLog auditLog = new AuditLog();
auditLog.setGroupId(groupId);
auditLog.setRoundId(roundId);
auditLog.setUserId(userId);
auditLog.setAction(action);
auditLog.setDetail(detail);
auditLog.setCreateTime(LocalDateTime.now());
auditLogMapper.insert(auditLog);
}
/**
* 分页查询审计日志
*/
@Override
public Page<AuditLog> getPage(Long groupId, Long roundId, String action,
Integer pageNum, Integer pageSize) {
Page<AuditLog> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<AuditLog> wrapper = new LambdaQueryWrapper<>();
if (groupId != null) {
wrapper.eq(AuditLog::getGroupId, groupId);
}
if (roundId != null) {
wrapper.eq(AuditLog::getRoundId, roundId);
}
if (action != null) {
wrapper.eq(AuditLog::getAction, action);
}
wrapper.orderByDesc(AuditLog::getCreateTime);
return auditLogMapper.selectPage(page, wrapper);
}
}
@@ -0,0 +1,153 @@
package com.tailbet.service.impl;
import com.tailbet.model.entity.BetOrder;
import com.tailbet.model.entity.User;
import com.tailbet.model.entity.GameRound;
import com.tailbet.mapper.BetOrderMapper;
import com.tailbet.mapper.UserMapper;
import com.tailbet.mapper.GameRoundMapper;
import com.tailbet.service.IBetService;
import com.tailbet.service.IGameService;
import com.tailbet.service.IPointsService;
import com.tailbet.service.IAuditService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 注单Service实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class BetServiceImpl implements IBetService {
private final BetOrderMapper betOrderMapper;
private final UserMapper userMapper;
private final GameRoundMapper gameRoundMapper;
private final IPointsService pointsService;
private final IAuditService auditService;
/**
* 下注
*/
@Override
@Transactional
public BetOrder placeBet(Long userId, Long groupId, String clientMsgId,
String playType, Integer betAmount) {
// 防重检查
if (clientMsgId != null) {
BetOrder existing = betOrderMapper.selectOne(new LambdaQueryWrapper<BetOrder>()
.eq(BetOrder::getClientMsgId, clientMsgId));
if (existing != null) {
throw new RuntimeException("重复下注");
}
}
// 检查用户
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (user.getIsFake() != null && user.getIsFake() == 1) {
// 假人下注只记录审计日志,不产生订单
auditService.log(groupId, null, userId, "FAKE_BET",
String.format("{\"playType\":\"%s\",\"amount\":%d}", playType, betAmount));
return null;
}
// 检查进行中的局
GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
.eq(GameRound::getGroupId, groupId)
.eq(GameRound::getStatus, 0));
if (round == null) {
throw new RuntimeException("当前没有进行中的局");
}
// 检查余额
if (user.getPoints() < betAmount) {
throw new RuntimeException("余额不足");
}
// 检查限额
if (betAmount < 1 || betAmount > 2000) {
throw new RuntimeException("单笔下注金额需在1-2000之间");
}
// 检查单局累计
Integer roundTotal = betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>()
.eq(BetOrder::getUserId, userId)
.eq(BetOrder::getRoundId, round.getId())
.eq(BetOrder::getIsFake, 0))
.stream()
.mapToInt(BetOrder::getBetAmount)
.sum();
if (roundTotal + betAmount > 20000) {
throw new RuntimeException("单局累计下注不能超过20000");
}
// 获取赔率
BigDecimal odds = getOdds(playType);
// 扣减积分
boolean success = pointsService.subPoints(userId, betAmount.longValue(),
IPointsService.TYPE_BET, null, null, "游戏下注");
if (!success) {
throw new RuntimeException("余额不足");
}
// 创建注单
BetOrder bet = new BetOrder();
bet.setUserId(userId);
bet.setGroupId(groupId);
bet.setRoundId(round.getId());
bet.setClientMsgId(clientMsgId);
bet.setPlayType(playType);
bet.setBetAmount(betAmount);
bet.setOdds(odds);
bet.setIsFake(0);
bet.setStatus(0);
bet.setCreateTime(LocalDateTime.now());
betOrderMapper.insert(bet);
// 审计日志
auditService.log(groupId, round.getId(), userId, "BET_PLACE",
String.format("{\"betId\":%d,\"playType\":\"%s\",\"amount\":%d}",
bet.getId(), playType, betAmount));
return bet;
}
/**
* 获取赔率
*/
@Override
public BigDecimal getOdds(String playType) {
if ("".equals(playType) || "".equals(playType)) {
return IGameService.ODDS_SINGLE_DOUBLE;
} else if ("".equals(playType) || "".equals(playType)) {
return IGameService.ODDS_SIZE;
} else {
return IGameService.ODDS_DIGIT;
}
}
/**
* 获取用户本局下注总额
*/
@Override
public Integer getUserRoundBetAmount(Long userId, Long roundId) {
return betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>()
.eq(BetOrder::getUserId, userId)
.eq(BetOrder::getRoundId, roundId)
.eq(BetOrder::getIsFake, 0))
.stream()
.mapToInt(BetOrder::getBetAmount)
.sum();
}
}
@@ -0,0 +1,177 @@
package com.tailbet.service.impl;
import com.tailbet.model.entity.User;
import com.tailbet.model.entity.GameRound;
import com.tailbet.mapper.UserMapper;
import com.tailbet.mapper.GameRoundMapper;
import com.tailbet.service.IFakeUserService;
import com.tailbet.service.IBetService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* 假人服务实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class FakeUserServiceImpl implements IFakeUserService {
private final UserMapper userMapper;
private final GameRoundMapper gameRoundMapper;
private final IBetService betService;
private final StringRedisTemplate redisTemplate;
private static final String FAKE_ACTIVE_KEY = "fake:active:";
private static final Random RANDOM = new Random();
// 假人昵称素材
private static final String[] NICKNAME_PREFIXES = {"", "", "", "", ""};
private static final String[] NICKNAME_SUFFIXES = {"", "", "", "", ""};
/**
* 初始化群假人
*/
@Override
public void initGroupFakeUsers(Long groupId, int minCount, int maxCount) {
int count = minCount + RANDOM.nextInt(maxCount - minCount + 1);
log.info("为群{}初始化{}个假人", groupId, count);
for (int i = 0; i < count; i++) {
createFakeUser(groupId);
}
}
/**
* 创建单个假人
*/
@Override
public User createFakeUser(Long groupId) {
User fake = new User();
fake.setUsername("fake_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12));
fake.setNickname(generateNickname());
fake.setPoints((long) (1000 + RANDOM.nextInt(49000)));
fake.setIsFake(1);
fake.setIsBot(0);
fake.setIsOps(0);
fake.setIsWhite(0);
fake.setStatus(1);
userMapper.insert(fake);
return fake;
}
/**
* 生成随机昵称
*/
private String generateNickname() {
String prefix = NICKNAME_PREFIXES[RANDOM.nextInt(NICKNAME_PREFIXES.length)];
String suffix = NICKNAME_SUFFIXES[RANDOM.nextInt(NICKNAME_SUFFIXES.length)];
int number = RANDOM.nextInt(1000);
return prefix + suffix + number;
}
/**
* 每日重置活跃假人
*/
@Override
@Scheduled(cron = "0 0 0 * * ?")
public void resetDailyActiveFakes() {
// 清理昨天的活跃标记
Set<String> keys = redisTemplate.opsForSet().members(FAKE_ACTIVE_KEY + "yesterday");
if (keys != null) {
for (String userId : keys) {
redisTemplate.opsForSet().remove(FAKE_ACTIVE_KEY + "yesterday", userId);
}
}
// 查询所有假人
List<User> fakes = userMapper.selectList(new LambdaQueryWrapper<User>()
.eq(User::getIsFake, 1)
.eq(User::getStatus, 1));
if (fakes.isEmpty()) {
return;
}
// 随机选15-20个作为今日活跃
int activeCount = 15 + RANDOM.nextInt(6);
activeCount = Math.min(activeCount, fakes.size());
Collections.shuffle(fakes);
List<String> activeUserIds = new ArrayList<>();
for (int i = 0; i < activeCount; i++) {
activeUserIds.add(String.valueOf(fakes.get(i).getId()));
}
// 存入Redis
String today = java.time.LocalDate.now().toString();
redisTemplate.opsForSet().add(FAKE_ACTIVE_KEY + today, activeUserIds.toArray(new String[0]));
log.info("今日活跃假人已更新: {}个", activeCount);
}
/**
* 检查用户是否是今日活跃假人
*/
@Override
public boolean isTodayActiveFake(Long userId) {
String today = java.time.LocalDate.now().toString();
Boolean isMember = redisTemplate.opsForSet().isMember(FAKE_ACTIVE_KEY + today, String.valueOf(userId));
return Boolean.TRUE.equals(isMember);
}
/**
* 假人下注
*/
@Override
public void fakeBet(Long groupId) {
// 获取今日活跃假人
String today = java.time.LocalDate.now().toString();
Set<String> activeFakes = redisTemplate.opsForSet().members(FAKE_ACTIVE_KEY + today);
if (activeFakes == null || activeFakes.isEmpty()) {
return;
}
// 随机选一个假人
List<String> fakeList = new ArrayList<>(activeFakes);
String fakeIdStr = fakeList.get(RANDOM.nextInt(fakeList.size()));
Long fakeId = Long.parseLong(fakeIdStr);
// 检查是否有进行中的局
GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
.eq(GameRound::getGroupId, groupId)
.eq(GameRound::getStatus, 0));
if (round == null) {
return;
}
// 随机玩法和金额
String[] playTypes = {"", "", "", ""};
String playType = playTypes[RANDOM.nextInt(playTypes.length)];
int amount = (1 + RANDOM.nextInt(10)) * 10; // 10,20,30...100
try {
betService.placeBet(fakeId, groupId, "fake_" + UUID.randomUUID(), playType, amount);
log.debug("假人{}下注: {} {}", fakeId, playType, amount);
} catch (Exception e) {
log.debug("假人下注失败: {}", e.getMessage());
}
}
/**
* 获取假人列表
*/
@Override
public List<User> getFakeUsers() {
return userMapper.selectList(new LambdaQueryWrapper<User>()
.eq(User::getIsFake, 1)
.eq(User::getStatus, 1));
}
}
@@ -0,0 +1,336 @@
package com.tailbet.service.impl;
import com.tailbet.entity.GameRound;
import com.tailbet.entity.DrawRecord;
import com.tailbet.entity.BetOrder;
import com.tailbet.entity.Group;
import com.tailbet.entity.User;
import com.tailbet.mapper.GameRoundMapper;
import com.tailbet.mapper.DrawRecordMapper;
import com.tailbet.mapper.BetOrderMapper;
import com.tailbet.mapper.GroupMapper;
import com.tailbet.mapper.UserMapper;
import com.tailbet.service.IGameService;
import com.tailbet.service.IPointsService;
import com.tailbet.service.IAuditService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* 游戏Service实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class GameServiceImpl implements IGameService {
private final GameRoundMapper gameRoundMapper;
private final DrawRecordMapper drawRecordMapper;
private final BetOrderMapper betOrderMapper;
private final GroupMapper groupMapper;
private final UserMapper userMapper;
private final IPointsService pointsService;
private final IAuditService auditService;
/**
* 获取群当前进行中的局
*/
@Override
public GameRound getOngoingRound(Long groupId) {
return gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
.eq(GameRound::getGroupId, groupId)
.eq(GameRound::getStatus, STATUS_ONGOING));
}
/**
* 生成期号
*/
@Override
public String generateRoundNo(Long groupId) {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// 查询当日该群最后一期
LambdaQueryWrapper<GameRound> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GameRound::getGroupId, groupId)
.likeRight(GameRound::getRoundNo, date)
.orderByDesc(GameRound::getCreateTime)
.last("LIMIT 1");
GameRound lastRound = gameRoundMapper.selectOne(wrapper);
int seq = 1;
if (lastRound != null) {
String lastNo = lastRound.getRoundNo();
String seqStr = lastNo.substring(lastNo.lastIndexOf("-") + 1);
seq = Integer.parseInt(seqStr) + 1;
}
return date + "-" + String.format("%02d", seq);
}
/**
* 开始游戏(群主操作)
*/
@Override
@Transactional
public GameRound startGame(Long groupId, Long userId) {
// 校验群主权限
Group group = groupMapper.selectById(groupId);
if (group == null || !group.getOwnerUserId().equals(userId)) {
throw new RuntimeException("只有群主可以开始游戏");
}
// 检查是否有进行中的局
if (getOngoingRound(groupId) != null) {
throw new RuntimeException("当前已有进行中的局");
}
// 创建新局
GameRound round = new GameRound();
round.setGroupId(groupId);
round.setRoundNo(generateRoundNo(groupId));
round.setStatus(STATUS_ONGOING);
round.setStartTime(LocalDateTime.now());
round.setDigitStrategy("random");
round.setAutoNext(1);
gameRoundMapper.insert(round);
// 审计日志
auditService.log(groupId, round.getId(), userId, "GAME_START",
String.format("{\"roundNo\":\"%s\"}", round.getRoundNo()));
return round;
}
/**
* 结束接注
*/
@Override
@Transactional
public GameRound endBet(Long groupId, Long userId) {
GameRound round = getOngoingRound(groupId);
if (round == null) {
throw new RuntimeException("没有进行中的局");
}
// 校验群主权限
Group group = groupMapper.selectById(groupId);
if (!group.getOwnerUserId().equals(userId)) {
throw new RuntimeException("只有群主可以结束接注");
}
round.setStatus(STATUS_ENDED);
round.setEndBetTime(LocalDateTime.now());
gameRoundMapper.updateById(round);
// 审计日志
auditService.log(groupId, round.getId(), userId, "GAME_END_BET", "{}");
return round;
}
/**
* 停止自动连开
*/
@Override
@Transactional
public void stopAutoNext(Long groupId, Long userId) {
GameRound round = getOngoingRound(groupId);
if (round == null) {
throw new RuntimeException("没有进行中的局");
}
Group group = groupMapper.selectById(groupId);
if (!group.getOwnerUserId().equals(userId)) {
throw new RuntimeException("只有群主可以停止自动连开");
}
round.setAutoNext(0);
gameRoundMapper.updateById(round);
auditService.log(groupId, round.getId(), userId, "STOP_AUTO_NEXT", "{}");
}
/**
* 设置目标尾数
*/
@Override
@Transactional
public void setTargetDigit(Long groupId, Integer digit, Long userId) {
GameRound round = getOngoingRound(groupId);
if (round == null) {
throw new RuntimeException("没有进行中的局");
}
round.setTargetDigit(digit);
round.setDigitStrategy("specified");
gameRoundMapper.updateById(round);
auditService.log(groupId, round.getId(), userId, "SET_DIGIT",
String.format("{\"digit\":%d}", digit));
}
/**
* 尾数试算
*/
@Override
public List<IGameService.DigitTrial> trialDigit(Long groupId, Long roundId) {
GameRound round;
if (roundId != null) {
round = gameRoundMapper.selectById(roundId);
} else {
round = getOngoingRound(groupId);
}
if (round == null) {
throw new RuntimeException("没有进行中的局");
}
// 获取本局所有注单
List<BetOrder> bets = betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>()
.eq(BetOrder::getRoundId, round.getId())
.eq(BetOrder::getIsFake, 0));
// 计算0-9各尾数的庄家利润
java.util.List<IGameService.DigitTrial> result = new java.util.ArrayList<>();
for (int d = 0; d <= 9; d++) {
IGameService.DigitTrial trial = new IGameService.DigitTrial();
trial.setDigit(d);
trial.setPlayWin(getPlayWinDesc(d));
int totalBet = 0;
long totalAward = 0;
for (BetOrder bet : bets) {
totalBet += bet.getBetAmount();
if (isWin(bet, d)) {
totalAward += bet.getBetAmount() * (1 + bet.getOdds().doubleValue());
}
}
trial.setTotalBet(totalBet);
trial.setTotalAward(totalAward);
trial.setProfit(totalBet - totalAward);
result.add(trial);
}
return result;
}
/**
* 判定是否中奖
*/
@Override
public boolean isWin(BetOrder bet, int digit) {
String playType = bet.getPlayType();
if ("".equals(playType)) {
return digit % 2 == 1; // 奇数为单
} else if ("".equals(playType)) {
return digit % 2 == 0; // 偶数为双(含0)
} else if ("".equals(playType)) {
return digit >= 5;
} else if ("".equals(playType)) {
return digit < 5;
} else if (playType.matches("[0-9]")) {
return Integer.parseInt(playType) == digit;
}
return false;
}
/**
* 获取中奖玩法描述
*/
@Override
public String getPlayWinDesc(int digit) {
StringBuilder sb = new StringBuilder();
sb.append(digit % 2 == 1 ? "" : "");
sb.append("/");
sb.append(digit >= 5 ? "" : "");
sb.append("/");
sb.append("数字").append(digit);
return sb.toString();
}
/**
* 开奖结算
*/
@Override
@Transactional
public void settle(Long roundId, int digit) {
GameRound round = gameRoundMapper.selectById(roundId);
if (round == null) {
throw new RuntimeException("局不存在");
}
// 获取本局所有注单
List<BetOrder> bets = betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>()
.eq(BetOrder::getRoundId, roundId)
.eq(BetOrder::getStatus, 0)); // 待结算
long totalBet = 0;
long totalAward = 0;
for (BetOrder bet : bets) {
totalBet += bet.getBetAmount();
if (isWin(bet, digit)) {
// 中奖,派奖
long winAmount = (long) (bet.getBetAmount() * (1 + bet.getOdds().doubleValue()));
bet.setStatus(1); // 已结算
bet.setWinAmount(winAmount);
betOrderMapper.updateById(bet);
// 给用户加积分
pointsService.addPoints(bet.getUserId(), winAmount, IPointsService.TYPE_AWARD,
String.valueOf(bet.getId()), null, "游戏中奖");
totalAward += winAmount;
} else {
// 未中奖
bet.setStatus(2); // 未中奖
betOrderMapper.updateById(bet);
}
}
// 记录开奖结果
DrawRecord record = new DrawRecord();
record.setGroupId(round.getGroupId());
record.setRoundNo(round.getRoundNo());
record.setRoundId(roundId);
record.setDigit(digit);
record.setPlayWins(getPlayWinDesc(digit));
record.setTotalBet((int) totalBet);
record.setTotalAward(totalAward);
record.setProfit(totalBet - totalAward);
record.setCreateTime(LocalDateTime.now());
drawRecordMapper.insert(record);
// 更新局状态
round.setFinishTime(LocalDateTime.now());
round.setStatus(STATUS_ENDED);
gameRoundMapper.updateById(round);
// 审计日志
auditService.log(round.getGroupId(), roundId, null, "SETTLE",
String.format("{\"digit\":%d,\"totalBet\":%d,\"totalAward\":%d,\"profit\":%d}",
digit, totalBet, totalAward, totalBet - totalAward));
}
/**
* 获取开奖历史
*/
@Override
public Page<DrawRecord> getHistory(Long groupId, Integer pageNum, Integer pageSize) {
Page<DrawRecord> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<DrawRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DrawRecord::getGroupId, groupId)
.orderByDesc(DrawRecord::getCreateTime);
return drawRecordMapper.selectPage(page, wrapper);
}
}
@@ -0,0 +1,214 @@
package com.tailbet.service.impl;
import com.tailbet.model.entity.Group;
import com.tailbet.model.entity.GroupMember;
import com.tailbet.mapper.GroupMapper;
import com.tailbet.mapper.GroupMemberMapper;
import com.tailbet.mapper.UserMapper;
import com.tailbet.service.IGroupService;
import com.tailbet.service.IAuditService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
* 群Service实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class GroupServiceImpl implements IGroupService {
private final GroupMapper groupMapper;
private final GroupMemberMapper groupMemberMapper;
private final UserMapper userMapper;
private final IAuditService auditService;
/**
* 获取群信息
*/
@Override
public Group getById(Long groupId) {
return groupMapper.selectById(groupId);
}
/**
* 获取用户的群列表
*/
@Override
public List<Group> getUserGroups(Long userId) {
List<GroupMember> members = groupMemberMapper.selectList(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getUserId, userId));
if (members.isEmpty()) {
return List.of();
}
List<Long> groupIds = members.stream()
.map(GroupMember::getGroupId)
.toList();
return groupMapper.selectList(new LambdaQueryWrapper<Group>()
.in(Group::getId, groupIds)
.eq(Group::getStatus, 1));
}
/**
* 拉用户入群
*/
@Override
@Transactional
public void pullUser(Long groupId, Long userId, Long operatorId) {
// 检查是否已在群中
GroupMember existing = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, userId));
if (existing != null) {
throw new RuntimeException("用户已在群中");
}
GroupMember member = new GroupMember();
member.setGroupId(groupId);
member.setUserId(userId);
member.setRole(ROLE_NORMAL);
member.setJoinTime(LocalDateTime.now());
member.setCreateTime(LocalDateTime.now());
member.setUpdateTime(LocalDateTime.now());
groupMemberMapper.insert(member);
// 审计日志
auditService.log(groupId, null, operatorId, "PULL_USER",
String.format("{\"userId\":%d}", userId));
}
/**
* 踢出用户
*/
@Override
@Transactional
public void kickUser(Long groupId, Long targetUserId, Long operatorId) {
Group group = groupMapper.selectById(groupId);
// 检查权限
GroupMember operator = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, operatorId));
if (operator == null) {
throw new RuntimeException("你不是群成员");
}
// 只有群主和管理员可以踢人
if (operator.getRole() != ROLE_OWNER && operator.getRole() != ROLE_ADMIN) {
throw new RuntimeException("只有群主和管理员可以踢人");
}
// 不能踢群主
if (group.getOwnerUserId().equals(targetUserId)) {
throw new RuntimeException("不能踢出群主");
}
// 删除成员
groupMemberMapper.delete(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, targetUserId));
// 审计日志
auditService.log(groupId, null, operatorId, "KICK_USER",
String.format("{\"targetUserId\":%d}", targetUserId));
}
/**
* 设置管理员
*/
@Override
@Transactional
public void setAdmin(Long groupId, Long targetUserId, Long operatorId) {
Group group = groupMapper.selectById(groupId);
// 只有群主可以设置管理员
if (!group.getOwnerUserId().equals(operatorId)) {
throw new RuntimeException("只有群主可以设置管理员");
}
GroupMember member = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, targetUserId));
if (member == null) {
throw new RuntimeException("目标用户不是群成员");
}
member.setRole(ROLE_ADMIN);
member.setUpdateTime(LocalDateTime.now());
groupMemberMapper.updateById(member);
// 审计日志
auditService.log(groupId, null, operatorId, "SET_ADMIN",
String.format("{\"targetUserId\":%d}", targetUserId));
}
/**
* 取消管理员
*/
@Override
@Transactional
public void removeAdmin(Long groupId, Long targetUserId, Long operatorId) {
Group group = groupMapper.selectById(groupId);
if (!group.getOwnerUserId().equals(operatorId)) {
throw new RuntimeException("只有群主可以取消管理员");
}
GroupMember member = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, targetUserId));
if (member == null) {
throw new RuntimeException("目标用户不是群成员");
}
member.setRole(ROLE_NORMAL);
member.setUpdateTime(LocalDateTime.now());
groupMemberMapper.updateById(member);
// 审计日志
auditService.log(groupId, null, operatorId, "REMOVE_ADMIN",
String.format("{\"targetUserId\":%d}", targetUserId));
}
/**
* 获取群成员
*/
@Override
public List<GroupMember> getMembers(Long groupId) {
return groupMemberMapper.selectList(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId));
}
/**
* 检查用户是否是群主
*/
@Override
public boolean isOwner(Long groupId, Long userId) {
Group group = groupMapper.selectById(groupId);
return group != null && group.getOwnerUserId().equals(userId);
}
/**
* 检查用户是否是管理员
*/
@Override
public boolean isAdmin(Long groupId, Long userId) {
GroupMember member = groupMemberMapper.selectOne(new LambdaQueryWrapper<GroupMember>()
.eq(GroupMember::getGroupId, groupId)
.eq(GroupMember::getUserId, userId));
return member != null && (member.getRole() == ROLE_ADMIN || member.getRole() == ROLE_OWNER);
}
}
@@ -0,0 +1,111 @@
package com.tailbet.service.impl;
import com.tailbet.model.entity.PointsFlow;
import com.tailbet.model.entity.User;
import com.tailbet.mapper.PointsFlowMapper;
import com.tailbet.mapper.UserMapper;
import com.tailbet.service.IPointsService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 积分Service实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class PointsServiceImpl implements IPointsService {
private final PointsFlowMapper pointsFlowMapper;
private final UserMapper userMapper;
/**
* 添加积分
*/
@Override
@Transactional
public void addPoints(Long userId, Long amount, String type, String bizNo, Long operatorId, String remark) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
Long before = user.getPoints();
Long after = before + amount;
user.setPoints(after);
userMapper.updateById(user);
// 记录流水
PointsFlow flow = new PointsFlow();
flow.setUserId(userId);
flow.setType(type);
flow.setAmount(amount);
flow.setBalanceBefore(before);
flow.setBalanceAfter(after);
flow.setBizNo(bizNo);
flow.setOperatorId(operatorId);
flow.setRemark(remark);
pointsFlowMapper.insert(flow);
}
/**
* 扣除积分
*/
@Override
@Transactional
public boolean subPoints(Long userId, Long amount, String type, String bizNo, Long operatorId, String remark) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (user.getPoints() < amount) {
return false;
}
Long before = user.getPoints();
Long after = before - amount;
user.setPoints(after);
userMapper.updateById(user);
// 记录流水
PointsFlow flow = new PointsFlow();
flow.setUserId(userId);
flow.setType(type);
flow.setAmount(-amount);
flow.setBalanceBefore(before);
flow.setBalanceAfter(after);
flow.setBizNo(bizNo);
flow.setOperatorId(operatorId);
flow.setRemark(remark);
pointsFlowMapper.insert(flow);
return true;
}
/**
* 获取用户积分
*/
@Override
public Long getPoints(Long userId) {
User user = userMapper.selectById(userId);
return user != null ? user.getPoints() : 0L;
}
/**
* 查询积分流水记录
*/
@Override
public Page<PointsFlow> getFlowPage(Long userId, Integer pageNum, Integer pageSize) {
Page<PointsFlow> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<PointsFlow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PointsFlow::getUserId, userId)
.orderByDesc(PointsFlow::getCreateTime);
return pointsFlowMapper.selectPage(page, wrapper);
}
}
@@ -0,0 +1,125 @@
package com.tailbet.service.impl;
import com.tailbet.entity.User;
import com.tailbet.entity.DigitWhite;
import com.tailbet.mapper.UserMapper;
import com.tailbet.mapper.DigitWhiteMapper;
import com.tailbet.openim.OpenIMClient;
import com.tailbet.service.IPushService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 推送服务实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class PushServiceImpl implements IPushService {
private final UserMapper userMapper;
private final DigitWhiteMapper digitWhiteMapper;
private final OpenIMClient openIMClient;
/**
* 推送尾数试算完成通知给白名单用户
*/
@Override
public void pushDigitTrialComplete(Long groupId, Long roundId) {
List<DigitWhite> whites = digitWhiteMapper.selectList(new LambdaQueryWrapper<DigitWhite>());
for (DigitWhite white : whites) {
User user = userMapper.selectById(white.getUserId());
if (user != null) {
PushService.PushMessage message = new PushService.PushMessage();
message.setType(PUSH_TYPE_DIGIT_TRIAL);
message.setTitle("尾数试算完成");
message.setContent("可前往修改尾数");
message.setUserId(user.getId());
message.setGroupId(groupId);
message.setRoundId(roundId);
sendPush(message);
}
}
}
/**
* 推送游戏开始通知
*/
@Override
public void pushGameStart(Long groupId, String roundNo) {
PushService.PushMessage message = new PushService.PushMessage();
message.setType(PUSH_TYPE_GAME_START);
message.setTitle("游戏开始");
message.setContent(roundNo + "期已开始下注");
message.setGroupId(groupId);
message.setRoundNo(roundNo);
// 推送给所有群成员
broadcastToGroup(groupId, message);
}
/**
* 推送开奖结果通知
*/
@Override
public void pushGameResult(Long groupId, String roundNo, int digit, String playWins) {
PushService.PushMessage message = new PushService.PushMessage();
message.setType(PUSH_TYPE_RESULT);
message.setTitle("开奖结果");
message.setContent(roundNo + "期开奖\n尾数: " + digit + "\n中奖玩法: " + playWins);
message.setGroupId(groupId);
message.setRoundNo(roundNo);
message.setDigit(digit);
// 广播给所有群成员
broadcastToGroup(groupId, message);
}
/**
* 发送单用户推送
*/
@Override
public void sendPush(PushService.PushMessage message) {
try {
User user = userMapper.selectById(message.getUserId());
if (user == null) {
log.warn("推送用户不存在: userId={}", message.getUserId());
return;
}
// TODO: 根据实际推送通道实现
// 1. 如果用户在线,通过OpenIM发送即时消息
// 2. 如果用户离线,通过FCM/APNs发送离线推送
// 通过OpenIM单聊发送
openIMClient.sendUserMessage(String.valueOf(user.getId()),
message.getTitle() + " - " + message.getContent());
log.info("推送发送成功: userId={}, type={}", message.getUserId(), message.getType());
} catch (Exception e) {
log.error("推送发送失败: userId={}, type={}, error={}",
message.getUserId(), message.getType(), e.getMessage());
}
}
/**
* 广播推送消息给群成员
*/
@Override
public void broadcastToGroup(Long groupId, PushService.PushMessage message) {
try {
// 通过OpenIM群消息发送
openIMClient.sendGroupMessage(String.valueOf(groupId),
message.getContent());
log.info("群广播发送成功: groupId={}, type={}", groupId, message.getType());
} catch (Exception e) {
log.error("群广播发送失败: groupId={}, type={}, error={}",
groupId, message.getType(), e.getMessage());
}
}
}
@@ -0,0 +1,205 @@
package com.tailbet.service.impl;
import com.tailbet.model.entity.RedPacket;
import com.tailbet.model.entity.RedPacketRecv;
import com.tailbet.model.entity.User;
import com.tailbet.mapper.RedPacketMapper;
import com.tailbet.mapper.RedPacketRecvMapper;
import com.tailbet.mapper.UserMapper;
import com.tailbet.mapper.GameRoundMapper;
import com.tailbet.service.IRedPacketService;
import com.tailbet.service.IPointsService;
import com.tailbet.service.IAuditService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.*;
/**
* 红包Service实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class RedPacketServiceImpl implements IRedPacketService {
private final RedPacketMapper redPacketMapper;
private final RedPacketRecvMapper redPacketRecvMapper;
private final UserMapper userMapper;
private final GameRoundMapper gameRoundMapper;
private final IPointsService pointsService;
private final IAuditService auditService;
/**
* 发红包
*/
@Override
@Transactional
public RedPacket sendRp(Long userId, Long groupId, BigDecimal totalAmount,
Integer totalCount, Long roundId) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
// 扣积分
boolean success = pointsService.subPoints(userId, totalAmount.longValue(),
IPointsService.TYPE_RP_SEND, null, null, "发送红包");
if (!success) {
throw new RuntimeException("余额不足");
}
// 创建红包
RedPacket rp = new RedPacket();
rp.setUserId(userId);
rp.setGroupId(groupId);
rp.setRoundId(roundId);
rp.setTotalAmount(totalAmount);
rp.setTotalCount(totalCount);
rp.setRemainAmount(totalAmount);
rp.setRemainCount(totalCount);
rp.setStatus(STATUS_ONGOING);
rp.setCreateTime(LocalDateTime.now());
// 判断红包类型
if (roundId != null) {
rp.setType(TYPE_GAME);
} else {
rp.setType(TYPE_RANDOM);
}
redPacketMapper.insert(rp);
// 审计日志
auditService.log(groupId, roundId, userId, "RP_SEND",
String.format("{\"rpId\":%d,\"amount\":%s,\"count\":%d}",
rp.getId(), totalAmount, totalCount));
return rp;
}
/**
* 领红包
*/
@Override
@Transactional
public RedPacketRecv receiveRp(Long rpId, Long userId) {
RedPacket rp = redPacketMapper.selectById(rpId);
if (rp == null) {
throw new RuntimeException("红包不存在");
}
if (rp.getStatus() != STATUS_ONGOING) {
throw new RuntimeException("红包已领完");
}
// 检查是否已领取
RedPacketRecv existing = redPacketRecvMapper.selectOne(new LambdaQueryWrapper<RedPacketRecv>()
.eq(RedPacketRecv::getRpId, rpId)
.eq(RedPacketRecv::getUserId, userId));
if (existing != null) {
throw new RuntimeException("已领取过该红包");
}
// 计算领取金额
BigDecimal amount = calculateReceiveAmount(rp);
// 更新红包
rp.setRemainAmount(rp.getRemainAmount().subtract(amount));
rp.setRemainCount(rp.getRemainCount() - 1);
if (rp.getRemainCount() <= 0) {
rp.setStatus(STATUS_FINISHED);
}
redPacketMapper.updateById(rp);
// 加积分
pointsService.addPoints(userId, amount.longValue(),
IPointsService.TYPE_RP_RECV, String.valueOf(rpId), null, "领取红包");
// 记录领取
RedPacketRecv recv = new RedPacketRecv();
recv.setRpId(rpId);
recv.setUserId(userId);
recv.setAmount(amount);
recv.setCreateTime(LocalDateTime.now());
redPacketRecvMapper.insert(recv);
// 审计日志
auditService.log(rp.getGroupId(), rp.getRoundId(), userId, "RP_RECV",
String.format("{\"rpId\":%d,\"amount\":%s}", rpId, amount));
return recv;
}
/**
* 计算领取金额
*/
@Override
public BigDecimal calculateReceiveAmount(RedPacket rp) {
if (rp.getRemainCount() <= 0) {
return BigDecimal.ZERO;
}
// 如果是游戏红包且有目标尾数,则需要控制手气王
// 简化实现:随机分配
Random random = new Random();
if (rp.getRemainCount() == 1) {
// 最后一个,拿剩余全部
return rp.getRemainAmount();
}
// 随机金额,最小0.01
double min = 0.01;
double max = rp.getRemainAmount().divide(BigDecimal.valueOf(rp.getRemainCount()), 2, RoundingMode.HALF_UP).doubleValue() * 2;
double amount = min + (max - min) * random.nextDouble();
return BigDecimal.valueOf(amount).setScale(2, RoundingMode.HALF_UP);
}
/**
* 获取红包详情
*/
@Override
public RedPacket getById(Long rpId) {
return redPacketMapper.selectById(rpId);
}
/**
* 获取红包领取列表
*/
@Override
public List<RedPacketRecv> getReceives(Long rpId) {
return redPacketRecvMapper.selectList(new LambdaQueryWrapper<RedPacketRecv>()
.eq(RedPacketRecv::getRpId, rpId)
.orderByAsc(RedPacketRecv::getCreateTime));
}
/**
* 查询红包记录
*/
@Override
public Page<RedPacket> getRecords(Long userId, Integer pageNum, Integer pageSize) {
Page<RedPacket> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<RedPacket> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(RedPacket::getUserId, userId)
.orderByDesc(RedPacket::getCreateTime);
return redPacketMapper.selectPage(page, wrapper);
}
/**
* 查找进行中的游戏红包
*/
@Override
public RedPacket getOngoingGameRp(Long groupId) {
return redPacketMapper.selectOne(new LambdaQueryWrapper<RedPacket>()
.eq(RedPacket::getGroupId, groupId)
.eq(RedPacket::getType, TYPE_GAME)
.eq(RedPacket::getStatus, STATUS_ONGOING)
.isNotNull(RedPacket::getRoundId));
}
}
@@ -0,0 +1,149 @@
package com.tailbet.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.tailbet.model.entity.User;
import com.tailbet.mapper.UserMapper;
import com.tailbet.model.dto.UpdateUserDTO;
import com.tailbet.service.IUserService;
import com.tailbet.util.JwtUtil;
import com.tailbet.util.PasswordUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 用户Service实现
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class UserServiceImpl implements IUserService {
private final UserMapper userMapper;
private final PasswordUtil passwordUtil;
private final JwtUtil jwtUtil;
@Override
public User getById(Long id) {
return userMapper.selectById(id);
}
/**
* 根据用户名查询用户
*/
@Override
public User getByUsername(String username) {
return userMapper.selectOne(new LambdaQueryWrapper<User>()
.eq(User::getUsername, username));
}
/**
* 注册用户
*/
@Override
public User register(String username, String password) {
// 检查用户名是否已存在
if (getByUsername(username) != null) {
throw new RuntimeException("用户名已存在");
}
User user = new User();
user.setUsername(username);
user.setPassword(passwordUtil.encode(password));
user.setNickname(username);
user.setPoints(0L);
user.setStatus(1);
userMapper.insert(user);
// 分配运营(按最少绑定+优先在线算法)
assignOps(user);
return user;
}
/**
* 用户登录
*/
@Override
public String login(String username, String password) {
User user = getByUsername(username);
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (user.getStatus() == 0) {
throw new RuntimeException("账号已被封禁");
}
if (!passwordUtil.matches(password, user.getPassword())) {
throw new RuntimeException("密码错误");
}
return jwtUtil.generateToken(user.getId(), user.getUsername());
}
/**
* 更新用户
*/
@Override
public boolean updateUser(User user) {
return userMapper.updateById(user) > 0;
}
/**
* 更新用户信息
*/
@Override
public void updateUserInfo(Long userId, UpdateUserDTO dto) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (dto.getNickname() != null && !dto.getNickname().isEmpty()) {
user.setNickname(dto.getNickname());
}
if (dto.getAvatarUrl() != null) {
user.setAvatarUrl(dto.getAvatarUrl());
}
if (dto.getPassword() != null && !dto.getPassword().isEmpty()) {
user.setPassword(passwordUtil.encode(dto.getPassword()));
}
userMapper.updateById(user);
}
/**
* 获取当前登录用户
*/
@Override
public User getCurrentUser() {
return null;
}
/**
* 分配运营(粘性绑定:在线+最少绑定)
*/
private void assignOps(User user) {
// 查询所有运营账号
List<User> opsList = userMapper.selectList(new LambdaQueryWrapper<User>()
.eq(User::getIsOps, 1)
.eq(User::getStatus, 1));
if (opsList == null || opsList.isEmpty()) {
log.warn("没有可用的运营账号,用户{}将无法分配运营", user.getUsername());
return;
}
// 找绑定用户最少的运营
User selectedOps = opsList.stream()
.min((a, b) -> {
Long countA = userMapper.selectCount(new LambdaQueryWrapper<User>()
.eq(User::getBoundOpsUserId, a.getId()));
Long countB = userMapper.selectCount(new LambdaQueryWrapper<User>()
.eq(User::getBoundOpsUserId, b.getId()));
return countA.compareTo(countB);
})
.orElse(opsList.get(0));
user.setBoundOpsUserId(selectedOps.getId());
userMapper.updateById(user);
}
}