init
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user