火箭基础功能

This commit is contained in:
tianfeng 2025-11-11 23:12:07 +08:00
parent 5328936e27
commit 4746377fc6
24 changed files with 1924 additions and 0 deletions

View File

@ -0,0 +1,48 @@
package com.red.circle.other.adapter.app;
import com.red.circle.other.app.dto.clientobject.RocketRewardCO;
import com.red.circle.other.app.dto.clientobject.RocketStatusCO;
import com.red.circle.other.app.dto.cmd.RocketRewardClaimCmd;
import com.red.circle.other.app.dto.cmd.RocketStatusQueryCmd;
import com.red.circle.other.app.service.RocketService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 火箭REST控制器
*
* @author system
* @date 2025-01-15
*/
@RestController
@RequestMapping("/api/rocket")
@RequiredArgsConstructor
public class RocketRestController {
private final RocketService rocketService;
/**
* 查询火箭状态
*/
@GetMapping("/status")
public RocketStatusCO queryStatus(@Valid RocketStatusQueryCmd cmd) {
return rocketService.queryStatus(cmd);
}
/**
* 领取奖励
*/
@PostMapping("/claim")
public RocketRewardCO claimReward(@Valid @RequestBody RocketRewardClaimCmd cmd) {
return rocketService.claimReward(cmd);
}
/**
* 手动触发发射 (测试用)
*/
@PostMapping("/manual-launch")
public void manualLaunch(@RequestParam Long roomId) {
rocketService.manualLaunch(roomId);
}
}

View File

@ -0,0 +1,110 @@
package com.red.circle.other.app.command.rocket;
import com.red.circle.other.app.dto.cmd.RocketEnergyAddCmd;
import com.red.circle.other.app.util.DistributedLockUtil;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.infra.database.cache.key.RocketKeys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
/**
* 火箭能量增加命令执行器
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketEnergyAddCmdExe {
private final RocketStatusGateway rocketStatusGateway;
private final DistributedLockUtil distributedLockUtil;
private final RocketLaunchCmdExe rocketLaunchCmdExe;
/**
* 幸运礼物加成比例
*/
private static final double LUCKY_BONUS_RATE = 0.04;
/**
* 执行能量增加
*/
public void execute(RocketEnergyAddCmd cmd) {
// 构建锁Key
String lockKey = RocketKeys.LOCK.getKey(cmd.getRoomId());
// 使用分布式锁执行
distributedLockUtil.executeWithLock(lockKey, 5L, () -> {
doAddEnergy(cmd);
return null;
}, "火箭能量增加操作过于频繁,请稍后重试");
}
/**
* 执行能量增加逻辑
*/
private void doAddEnergy(RocketEnergyAddCmd cmd) {
// 1. 查询当前火箭状态
String today = LocalDate.now().toString();
RocketStatus rocketStatus = rocketStatusGateway.findByRoomIdAndDate(cmd.getRoomId(), today);
if (rocketStatus == null) {
// 初始化火箭
rocketStatus = RocketStatus.init(cmd.getRoomId());
log.info("初始化房间火箭, roomId={}", cmd.getRoomId());
}
// 2. 校验状态
if (!rocketStatus.canAddEnergy()) {
log.warn("火箭不可充能, roomId={}, status={}", cmd.getRoomId(), rocketStatus.getStatus());
return;
}
// 3. 计算能量值
long energyValue = calculateEnergy(cmd);
// 4. 增加能量
rocketStatus.addEnergy(
cmd.getUserId(),
cmd.getUserName(),
cmd.getUserAvatar(),
energyValue
);
// 5. 保存状态
rocketStatusGateway.save(rocketStatus);
log.info("火箭能量增加成功, roomId={}, userId={}, energy={}, currentEnergy={}, level={}",
cmd.getRoomId(), cmd.getUserId(), energyValue,
rocketStatus.getCurrentEnergy(), rocketStatus.getLevel().getLevel());
// 6. 检查是否需要发射
if (rocketStatus.isFullEnergy()) {
log.info("火箭能量已满, 准备发射, roomId={}, level={}, finalEnergy={}",
cmd.getRoomId(), rocketStatus.getLevel().getLevel(), rocketStatus.getCurrentEnergy());
// 触发发射
rocketLaunchCmdExe.execute(cmd.getRoomId());
}
}
/**
* 计算能量值
*/
private long calculateEnergy(RocketEnergyAddCmd cmd) {
long baseEnergy = cmd.getGiftGoldValue();
if (Boolean.TRUE.equals(cmd.getIsLucky())) {
// 幸运礼物额外增加4%
long luckyBonus = (long) (baseEnergy * LUCKY_BONUS_RATE);
return baseEnergy + luckyBonus;
}
return baseEnergy;
}
}

View File

@ -0,0 +1,87 @@
package com.red.circle.other.app.command.rocket;
import com.red.circle.other.app.util.DistributedLockUtil;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.domain.rocket.RocketStatusEnum;
import com.red.circle.other.infra.database.cache.key.RocketKeys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
/**
* 火箭发射命令执行器
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketLaunchCmdExe {
private final RocketStatusGateway rocketStatusGateway;
private final DistributedLockUtil distributedLockUtil;
/**
* 执行火箭发射
*/
public void execute(Long roomId) {
// 构建锁Key
String lockKey = RocketKeys.LOCK.getKey(roomId);
// 使用分布式锁执行
distributedLockUtil.executeWithLock(lockKey, 5L, () -> {
doLaunch(roomId);
return null;
}, "火箭发射操作过于频繁,请稍后重试");
}
/**
* 执行发射逻辑
*/
private void doLaunch(Long roomId) {
// 1. 查询当前火箭状态
String today = LocalDate.now().toString();
RocketStatus rocketStatus = rocketStatusGateway.findByRoomIdAndDate(roomId, today);
if (rocketStatus == null) {
log.warn("火箭状态不存在, roomId={}", roomId);
return;
}
// 2. 校验状态
if (rocketStatus.getStatus() != RocketStatusEnum.CHARGING) {
log.warn("火箭状态不正确, 无法发射, roomId={}, status={}", roomId, rocketStatus.getStatus());
return;
}
// 3. 校验能量
if (!rocketStatus.isFullEnergy()) {
log.warn("火箭能量不足, 无法发射, roomId={}, currentEnergy={}, maxEnergy={}",
roomId, rocketStatus.getCurrentEnergy(), rocketStatus.getMaxEnergy());
return;
}
// 4. 发射火箭
rocketStatus.launch();
// 5. 立即进入领取状态
rocketStatus.startClaiming();
// 6. 保存状态
rocketStatusGateway.save(rocketStatus);
log.info("火箭发射成功, roomId={}, level={}, finalEnergy={}, contributors={}, launchTime={}",
roomId,
rocketStatus.getLevel().getLevel(),
rocketStatus.getCurrentEnergy(),
rocketStatus.getContributors() != null ? rocketStatus.getContributors().size() : 0,
rocketStatus.getLaunchTime());
// TODO: 7. 推送发射动画 (WebSocket)
// TODO: 8. 保存历史记录
}
}

View File

@ -0,0 +1,119 @@
package com.red.circle.other.app.command.rocket;
import com.red.circle.other.app.dto.clientobject.RocketRewardCO;
import com.red.circle.other.app.dto.cmd.RocketRewardClaimCmd;
import com.red.circle.other.app.util.DistributedLockUtil;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketContributor;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.infra.database.cache.key.RocketKeys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
/**
* 火箭奖励领取命令执行器
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketRewardClaimCmdExe {
private final RocketStatusGateway rocketStatusGateway;
private final DistributedLockUtil distributedLockUtil;
// TODO: 后续需要注入 WalletGoldClient 调用钱包服务
/**
* 执行奖励领取
*/
public RocketRewardCO execute(RocketRewardClaimCmd cmd) {
// 构建锁Key (防止同一用户并发领取)
String lockKey = RocketKeys.LOCK.getKey(cmd.getRoomId(), "claim", cmd.getUserId());
return distributedLockUtil.executeWithLock(lockKey, 3L, () -> {
return doClaim(cmd);
}, "奖励领取操作过于频繁,请稍后重试");
}
/**
* 执行领取逻辑
*/
private RocketRewardCO doClaim(RocketRewardClaimCmd cmd) {
// 1. 查询火箭状态
String today = LocalDate.now().toString();
RocketStatus rocketStatus = rocketStatusGateway.findByRoomIdAndDate(cmd.getRoomId(), today);
if (rocketStatus == null) {
throw new RuntimeException("火箭状态不存在");
}
// 2. 校验领取资格
if (!rocketStatus.canClaim()) {
if (rocketStatus.isExpired()) {
throw new RuntimeException("奖励领取已过期");
}
throw new RuntimeException("当前状态不可领取奖励");
}
// 3. 检查是否已领取 (TODO: 需要查询MySQL防重)
// boolean hasClaimed = checkHasClaimed(cmd.getRoomId(), cmd.getUserId(), rocketStatus.getId());
// if (hasClaimed) {
// throw new RuntimeException("您已经领取过奖励了");
// }
// 4. 判断是否为贡献者
RocketContributor contributor = findContributor(rocketStatus, cmd.getUserId());
boolean isContributor = contributor != null;
// 5. 计算奖励 (TODO: 奖励配置后续补充)
RocketRewardCO rewardCO = new RocketRewardCO();
rewardCO.setIsContributor(isContributor);
if (isContributor) {
// 贡献者奖励
rewardCO.setContributionEnergy(contributor.getEnergy());
rewardCO.setContributionRate(contributor.getContributionRate());
rewardCO.setRewardType("GOLD");
rewardCO.setRewardAmount(1000L); // TODO: 根据配置计算
rewardCO.setMessage("恭喜您获得贡献者奖励!");
} else {
// 普通用户奖励
rewardCO.setRewardType("GOLD");
rewardCO.setRewardAmount(100L); // TODO: 根据配置计算
rewardCO.setMessage("恭喜您获得参与奖励!");
}
// 6. 发放奖励 (TODO: 调用钱包服务)
// String bizNo = "ROCKET_" + rocketStatus.getId() + "_" + cmd.getUserId();
// walletGoldClient.income(...);
// 7. 记录领取日志 (TODO: 保存到MySQL)
// 8. 更新领取人数
rocketStatus.incrementClaimCount();
rocketStatusGateway.save(rocketStatus);
log.info("火箭奖励领取成功, roomId={}, userId={}, isContributor={}, rewardAmount={}",
cmd.getRoomId(), cmd.getUserId(), isContributor, rewardCO.getRewardAmount());
return rewardCO;
}
/**
* 查找贡献者
*/
private RocketContributor findContributor(RocketStatus rocketStatus, Long userId) {
if (rocketStatus.getContributors() == null) {
return null;
}
return rocketStatus.getContributors().stream()
.filter(c -> c.getUserId().equals(userId))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,159 @@
package com.red.circle.other.app.command.rocket;
import com.red.circle.other.app.dto.clientobject.RocketContributorCO;
import com.red.circle.other.app.dto.clientobject.RocketStatusCO;
import com.red.circle.other.app.dto.cmd.RocketStatusQueryCmd;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketContributor;
import com.red.circle.other.domain.rocket.RocketStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 火箭状态查询执行器
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketStatusQryExe {
private final RocketStatusGateway rocketStatusGateway;
/**
* Top排行显示数量
*/
private static final int TOP_DISPLAY_COUNT = 10;
/**
* 执行查询
*/
public RocketStatusCO execute(RocketStatusQueryCmd cmd) {
// 1. 查询火箭状态
String today = LocalDate.now().toString();
RocketStatus rocketStatus = rocketStatusGateway.findByRoomIdAndDate(cmd.getRoomId(), today);
if (rocketStatus == null) {
// 返回初始状态
return buildInitialStatus(cmd.getRoomId());
}
// 2. 转换为CO
RocketStatusCO statusCO = convertToStatusCO(rocketStatus);
// 3. 填充当前用户相关信息
if (cmd.getCurrentUserId() != null) {
fillUserInfo(statusCO, rocketStatus, cmd.getCurrentUserId());
}
return statusCO;
}
/**
* 构建初始状态
*/
private RocketStatusCO buildInitialStatus(Long roomId) {
RocketStatusCO statusCO = new RocketStatusCO();
statusCO.setRoomId(roomId);
statusCO.setDate(LocalDate.now().toString());
statusCO.setLevel(1);
statusCO.setLevelName("一级火箭");
statusCO.setCurrentEnergy(0L);
statusCO.setMaxEnergy(150000L);
statusCO.setEnergyPercent(java.math.BigDecimal.ZERO);
statusCO.setStatus("CHARGING");
statusCO.setStatusDesc("充能中");
statusCO.setTopContributors(new ArrayList<>());
statusCO.setTotalContributors(0);
statusCO.setClaimCount(0);
statusCO.setCanClaim(false);
statusCO.setHasClaimed(false);
return statusCO;
}
/**
* 转换为CO
*/
private RocketStatusCO convertToStatusCO(RocketStatus rocketStatus) {
RocketStatusCO statusCO = new RocketStatusCO();
statusCO.setRoomId(rocketStatus.getRoomId());
statusCO.setDate(rocketStatus.getDate());
statusCO.setLevel(rocketStatus.getLevel().getLevel());
statusCO.setLevelName(rocketStatus.getLevel().getName());
statusCO.setCurrentEnergy(rocketStatus.getCurrentEnergy());
statusCO.setMaxEnergy(rocketStatus.getMaxEnergy());
statusCO.setEnergyPercent(rocketStatus.getEnergyPercent());
statusCO.setStatus(rocketStatus.getStatus().name());
statusCO.setStatusDesc(rocketStatus.getStatus().getDesc());
statusCO.setLaunchTime(rocketStatus.getLaunchTime());
statusCO.setClaimExpireTime(rocketStatus.getClaimExpireTime());
statusCO.setClaimCount(rocketStatus.getClaimCount());
// 转换Top贡献者
if (rocketStatus.getContributors() != null && !rocketStatus.getContributors().isEmpty()) {
List<RocketContributorCO> topContributors = rocketStatus.getContributors().stream()
.sorted(Comparator.comparing(RocketContributor::getEnergy).reversed())
.limit(TOP_DISPLAY_COUNT)
.map(this::convertToContributorCO)
.collect(Collectors.toList());
// 设置排名
for (int i = 0; i < topContributors.size(); i++) {
topContributors.get(i).setRank(i + 1);
}
statusCO.setTopContributors(topContributors);
statusCO.setTotalContributors(rocketStatus.getContributors().size());
} else {
statusCO.setTopContributors(new ArrayList<>());
statusCO.setTotalContributors(0);
}
return statusCO;
}
/**
* 转换贡献者CO
*/
private RocketContributorCO convertToContributorCO(RocketContributor contributor) {
RocketContributorCO contributorCO = new RocketContributorCO();
contributorCO.setUserId(contributor.getUserId());
contributorCO.setUserName(contributor.getUserName());
contributorCO.setUserAvatar(contributor.getUserAvatar());
contributorCO.setEnergy(contributor.getEnergy());
contributorCO.setContributionRate(contributor.getContributionRate());
contributorCO.setGiftCount(contributor.getGiftCount());
return contributorCO;
}
/**
* 填充当前用户信息
*/
private void fillUserInfo(RocketStatusCO statusCO, RocketStatus rocketStatus, Long userId) {
// 判断是否可领取
statusCO.setCanClaim(rocketStatus.canClaim());
// TODO: 查询是否已领取 (需要查询MySQL)
statusCO.setHasClaimed(false);
// 查找当前用户贡献
if (rocketStatus.getContributors() != null) {
rocketStatus.getContributors().stream()
.filter(c -> c.getUserId().equals(userId))
.findFirst()
.ifPresent(contributor -> {
statusCO.setMyContribution(contributor.getEnergy());
statusCO.setMyContributionRate(contributor.getContributionRate());
});
}
}
}

View File

@ -0,0 +1,57 @@
package com.red.circle.other.app.scheduler;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.domain.rocket.RocketStatusEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
/**
* 火箭领取过期检查任务
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketClaimExpireTask {
private final RocketStatusGateway rocketStatusGateway;
/**
* 每分钟检查一次过期状态
*/
@Scheduled(fixedRate = 60000)
public void checkExpire() {
try {
String today = LocalDate.now().toString();
// 查询所有处于充能状态的火箭 (包含CLAIMING状态)
List<RocketStatus> allRockets = rocketStatusGateway.findAllCharging(today);
for (RocketStatus rocketStatus : allRockets) {
// 检查是否过期
if (rocketStatus.getStatus() == RocketStatusEnum.CLAIMING && rocketStatus.isExpired()) {
log.info("火箭领取已过期, 开始重置, roomId={}, claimExpireTime={}",
rocketStatus.getRoomId(), rocketStatus.getClaimExpireTime());
// TODO: 保存历史记录
// 重置火箭
rocketStatus.reset();
rocketStatusGateway.save(rocketStatus);
log.info("过期火箭重置成功, roomId={}", rocketStatus.getRoomId());
}
}
} catch (Exception e) {
log.error("火箭过期检查任务执行失败", e);
}
}
}

View File

@ -0,0 +1,61 @@
package com.red.circle.other.app.scheduler;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
/**
* 火箭每日重置定时任务
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RocketDailyResetTask {
private final RocketStatusGateway rocketStatusGateway;
/**
* 每天00:00执行重置
*/
@Scheduled(cron = "0 0 0 * * ?")
public void resetDaily() {
log.info("开始执行火箭每日重置任务");
try {
String today = LocalDate.now().toString();
// 查询所有处于充能状态的火箭
List<RocketStatus> chargingRockets = rocketStatusGateway.findAllCharging(today);
log.info("找到需要重置的火箭数量: {}", chargingRockets.size());
// 重置每个火箭
for (RocketStatus rocketStatus : chargingRockets) {
try {
// TODO: 保存昨日历史记录
// 重置状态
rocketStatus.reset();
rocketStatusGateway.save(rocketStatus);
log.info("火箭重置成功, roomId={}", rocketStatus.getRoomId());
} catch (Exception e) {
log.error("火箭重置失败, roomId={}", rocketStatus.getRoomId(), e);
}
}
log.info("火箭每日重置任务执行完成");
} catch (Exception e) {
log.error("火箭每日重置任务执行失败", e);
}
}
}

View File

@ -0,0 +1,51 @@
package com.red.circle.other.app.service;
import com.red.circle.other.app.command.rocket.RocketEnergyAddCmdExe;
import com.red.circle.other.app.command.rocket.RocketLaunchCmdExe;
import com.red.circle.other.app.command.rocket.RocketRewardClaimCmdExe;
import com.red.circle.other.app.command.rocket.RocketStatusQryExe;
import com.red.circle.other.app.dto.clientobject.RocketRewardCO;
import com.red.circle.other.app.dto.clientobject.RocketStatusCO;
import com.red.circle.other.app.dto.cmd.RocketEnergyAddCmd;
import com.red.circle.other.app.dto.cmd.RocketRewardClaimCmd;
import com.red.circle.other.app.dto.cmd.RocketStatusQueryCmd;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 火箭服务实现
*
* @author system
* @date 2025-01-15
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RocketServiceImpl implements RocketService {
private final RocketEnergyAddCmdExe rocketEnergyAddCmdExe;
private final RocketStatusQryExe rocketStatusQryExe;
private final RocketRewardClaimCmdExe rocketRewardClaimCmdExe;
private final RocketLaunchCmdExe rocketLaunchCmdExe;
@Override
public void addEnergy(RocketEnergyAddCmd cmd) {
rocketEnergyAddCmdExe.execute(cmd);
}
@Override
public RocketStatusCO queryStatus(RocketStatusQueryCmd cmd) {
return rocketStatusQryExe.execute(cmd);
}
@Override
public RocketRewardCO claimReward(RocketRewardClaimCmd cmd) {
return rocketRewardClaimCmdExe.execute(cmd);
}
@Override
public void manualLaunch(Long roomId) {
rocketLaunchCmdExe.execute(roomId);
}
}

View File

@ -0,0 +1,50 @@
package com.red.circle.other.app.dto.clientobject;
import lombok.Data;
import java.math.BigDecimal;
/**
* 火箭贡献者客户端对象
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketContributorCO {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名称
*/
private String userName;
/**
* 用户头像
*/
private String userAvatar;
/**
* 贡献能量
*/
private Long energy;
/**
* 贡献率 (%)
*/
private BigDecimal contributionRate;
/**
* 排名
*/
private Integer rank;
/**
* 送礼次数
*/
private Integer giftCount;
}

View File

@ -0,0 +1,50 @@
package com.red.circle.other.app.dto.clientobject;
import lombok.Data;
import java.math.BigDecimal;
/**
* 火箭奖励客户端对象
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketRewardCO {
/**
* 奖励类型 (GOLD/DIAMOND/PROP)
*/
private String rewardType;
/**
* 奖励物品ID
*/
private Long rewardItemId;
/**
* 奖励数量
*/
private Long rewardAmount;
/**
* 是否贡献者
*/
private Boolean isContributor;
/**
* 贡献能量
*/
private Long contributionEnergy;
/**
* 贡献率
*/
private BigDecimal contributionRate;
/**
* 提示信息
*/
private String message;
}

View File

@ -0,0 +1,107 @@
package com.red.circle.other.app.dto.clientobject;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 火箭状态客户端对象
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketStatusCO {
/**
* 房间ID
*/
private Long roomId;
/**
* 日期
*/
private String date;
/**
* 当前等级
*/
private Integer level;
/**
* 等级名称
*/
private String levelName;
/**
* 当前能量值
*/
private Long currentEnergy;
/**
* 最大能量值
*/
private Long maxEnergy;
/**
* 能量百分比
*/
private BigDecimal energyPercent;
/**
* 状态 (CHARGING/LAUNCHED/CLAIMING)
*/
private String status;
/**
* 状态描述
*/
private String statusDesc;
/**
* 贡献者排行 (Top 10)
*/
private List<RocketContributorCO> topContributors;
/**
* 总贡献人数
*/
private Integer totalContributors;
/**
* 发射时间
*/
private LocalDateTime launchTime;
/**
* 领取过期时间
*/
private LocalDateTime claimExpireTime;
/**
* 已领取人数
*/
private Integer claimCount;
/**
* 当前用户是否可领取
*/
private Boolean canClaim;
/**
* 当前用户是否已领取
*/
private Boolean hasClaimed;
/**
* 当前用户贡献能量
*/
private Long myContribution;
/**
* 当前用户贡献率
*/
private BigDecimal myContributionRate;
}

View File

@ -0,0 +1,57 @@
package com.red.circle.other.app.dto.cmd;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 火箭能量增加命令
*
* @author system
* @date 2025-01-15
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RocketEnergyAddCmd extends AppExtCommand {
/**
* 房间ID
*/
@NotNull(message = "房间ID不能为空")
private Long roomId;
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空")
private Long userId;
/**
* 用户名称
*/
private String userName;
/**
* 用户头像
*/
private String userAvatar;
/**
* 礼物金币价值
*/
@NotNull(message = "礼物价值不能为空")
private Long giftGoldValue;
/**
* 是否幸运礼物
*/
private Boolean isLucky = false;
/**
* 业务单号 (幂等)
*/
@NotBlank(message = "业务单号不能为空")
private String bizNo;
}

View File

@ -0,0 +1,33 @@
package com.red.circle.other.app.dto.cmd;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 火箭奖励领取命令
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketRewardClaimCmd {
/**
* 房间ID
*/
@NotNull(message = "房间ID不能为空")
private Long roomId;
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空")
private Long userId;
/**
* 火箭历史ID (MongoDB _id)
*/
@NotBlank(message = "火箭历史ID不能为空")
private String rocketHistoryId;
}

View File

@ -0,0 +1,25 @@
package com.red.circle.other.app.dto.cmd;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 火箭状态查询命令
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketStatusQueryCmd {
/**
* 房间ID
*/
@NotNull(message = "房间ID不能为空")
private Long roomId;
/**
* 当前用户ID (可选用于判断是否可领取)
*/
private Long currentUserId;
}

View File

@ -0,0 +1,36 @@
package com.red.circle.other.app.service;
import com.red.circle.other.app.dto.clientobject.RocketRewardCO;
import com.red.circle.other.app.dto.clientobject.RocketStatusCO;
import com.red.circle.other.app.dto.cmd.RocketEnergyAddCmd;
import com.red.circle.other.app.dto.cmd.RocketRewardClaimCmd;
import com.red.circle.other.app.dto.cmd.RocketStatusQueryCmd;
/**
* 火箭服务接口
*
* @author system
* @date 2025-01-15
*/
public interface RocketService {
/**
* 增加火箭能量
*/
void addEnergy(RocketEnergyAddCmd cmd);
/**
* 查询火箭状态
*/
RocketStatusCO queryStatus(RocketStatusQueryCmd cmd);
/**
* 领取奖励
*/
RocketRewardCO claimReward(RocketRewardClaimCmd cmd);
/**
* 手动触发火箭发射 (测试用)
*/
void manualLaunch(Long roomId);
}

View File

@ -0,0 +1,39 @@
package com.red.circle.other.domain.gateway;
import com.red.circle.other.domain.rocket.RocketStatus;
import java.util.List;
/**
* 火箭状态Gateway
*
* @author system
* @date 2025-01-15
*/
public interface RocketStatusGateway {
/**
* 根据房间ID和日期查询火箭状态
*/
RocketStatus findByRoomIdAndDate(Long roomId, String date);
/**
* 保存火箭状态
*/
void save(RocketStatus rocketStatus);
/**
* 批量查询房间火箭状态
*/
List<RocketStatus> findByRoomIds(List<Long> roomIds, String date);
/**
* 查询所有处于充能状态的火箭
*/
List<RocketStatus> findAllCharging(String date);
/**
* 删除火箭状态
*/
void delete(String id);
}

View File

@ -0,0 +1,76 @@
package com.red.circle.other.domain.rocket;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 贡献者值对象
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketContributor {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名称
*/
private String userName;
/**
* 用户头像
*/
private String userAvatar;
/**
* 贡献能量值
*/
private Long energy;
/**
* 贡献率 (%)
*/
private BigDecimal contributionRate;
/**
* 送礼次数
*/
private Integer giftCount;
/**
* 首次贡献时间
*/
private LocalDateTime firstContributeTime;
/**
* 最后贡献时间
*/
private LocalDateTime lastContributeTime;
/**
* 增加能量
*/
public void addEnergy(Long addEnergy) {
if (this.energy == null) {
this.energy = 0L;
}
this.energy += addEnergy;
if (this.giftCount == null) {
this.giftCount = 0;
}
this.giftCount++;
this.lastContributeTime = LocalDateTime.now();
if (this.firstContributeTime == null) {
this.firstContributeTime = LocalDateTime.now();
}
}
}

View File

@ -0,0 +1,78 @@
package com.red.circle.other.domain.rocket;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 火箭等级枚举
*
* @author system
* @date 2025-01-15
*/
@Getter
@AllArgsConstructor
public enum RocketLevel {
/**
* 1级火箭15万能量
*/
LEVEL_1(1, 150000L, "一级火箭"),
/**
* 2级火箭50万能量
*/
LEVEL_2(2, 500000L, "二级火箭"),
/**
* 3级火箭100万能量
*/
LEVEL_3(3, 1000000L, "三级火箭");
/**
* 等级值
*/
private final Integer level;
/**
* 最大能量值
*/
private final Long maxEnergy;
/**
* 等级名称
*/
private final String name;
/**
* 根据等级获取枚举
*/
public static RocketLevel of(Integer level) {
for (RocketLevel rocketLevel : values()) {
if (rocketLevel.getLevel().equals(level)) {
return rocketLevel;
}
}
throw new IllegalArgumentException("不支持的火箭等级: " + level);
}
/**
* 根据能量值判断等级
*/
public static RocketLevel determineLevel(Long energy) {
if (energy >= LEVEL_3.getMaxEnergy()) {
return LEVEL_3;
} else if (energy >= LEVEL_2.getMaxEnergy()) {
return LEVEL_2;
} else if (energy >= LEVEL_1.getMaxEnergy()) {
return LEVEL_1;
}
return LEVEL_1;
}
/**
* 检查能量是否达到当前等级阈值
*/
public boolean isReached(Long energy) {
return energy >= this.maxEnergy;
}
}

View File

@ -0,0 +1,266 @@
package com.red.circle.other.domain.rocket;
import lombok.Data;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* 火箭状态领域对象
*
* @author system
* @date 2025-01-15
*/
@Data
public class RocketStatus {
/**
* 主键ID
*/
private String id;
/**
* 房间ID
*/
private Long roomId;
/**
* 日期
*/
private String date;
/**
* 当前等级
*/
private RocketLevel level;
/**
* 当前能量值
*/
private Long currentEnergy;
/**
* 最大能量值
*/
private Long maxEnergy;
/**
* 状态
*/
private RocketStatusEnum status;
/**
* 贡献者列表
*/
private List<RocketContributor> contributors;
/**
* 发射时间
*/
private LocalDateTime launchTime;
/**
* 领取过期时间
*/
private LocalDateTime claimExpireTime;
/**
* 已领取人数
*/
private Integer claimCount;
/**
* 创建时间
*/
private LocalDateTime createdAt;
/**
* 更新时间
*/
private LocalDateTime updatedAt;
/**
* 初始化火箭
*/
public static RocketStatus init(Long roomId) {
RocketStatus rocketStatus = new RocketStatus();
rocketStatus.setRoomId(roomId);
rocketStatus.setDate(LocalDate.now().toString());
rocketStatus.setLevel(RocketLevel.LEVEL_1);
rocketStatus.setCurrentEnergy(0L);
rocketStatus.setMaxEnergy(RocketLevel.LEVEL_1.getMaxEnergy());
rocketStatus.setStatus(RocketStatusEnum.CHARGING);
rocketStatus.setContributors(new ArrayList<>());
rocketStatus.setClaimCount(0);
rocketStatus.setCreatedAt(LocalDateTime.now());
rocketStatus.setUpdatedAt(LocalDateTime.now());
return rocketStatus;
}
/**
* 增加能量
*/
public void addEnergy(Long userId, String userName, String userAvatar, Long energyValue) {
// 增加总能量
this.currentEnergy += energyValue;
// 更新贡献者
RocketContributor contributor = findContributor(userId);
if (contributor == null) {
contributor = new RocketContributor();
contributor.setUserId(userId);
contributor.setUserName(userName);
contributor.setUserAvatar(userAvatar);
contributor.setEnergy(0L);
contributor.setGiftCount(0);
this.contributors.add(contributor);
}
contributor.addEnergy(energyValue);
// 重新计算贡献率
recalculateContributionRate();
// 检查等级提升
checkAndUpgradeLevel();
this.updatedAt = LocalDateTime.now();
}
/**
* 发射火箭
*/
public void launch() {
this.status = RocketStatusEnum.LAUNCHED;
this.launchTime = LocalDateTime.now();
// 领取过期时间发射后1分钟
this.claimExpireTime = LocalDateTime.now().plusMinutes(1);
this.updatedAt = LocalDateTime.now();
}
/**
* 开始领取
*/
public void startClaiming() {
this.status = RocketStatusEnum.CLAIMING;
this.updatedAt = LocalDateTime.now();
}
/**
* 增加领取人数
*/
public void incrementClaimCount() {
if (this.claimCount == null) {
this.claimCount = 0;
}
this.claimCount++;
this.updatedAt = LocalDateTime.now();
}
/**
* 重置状态
*/
public void reset() {
this.date = LocalDate.now().toString();
this.level = RocketLevel.LEVEL_1;
this.currentEnergy = 0L;
this.maxEnergy = RocketLevel.LEVEL_1.getMaxEnergy();
this.status = RocketStatusEnum.CHARGING;
this.contributors = new ArrayList<>();
this.launchTime = null;
this.claimExpireTime = null;
this.claimCount = 0;
this.updatedAt = LocalDateTime.now();
}
/**
* 是否能量已满
*/
public boolean isFullEnergy() {
return this.currentEnergy >= this.maxEnergy;
}
/**
* 是否可以充能
*/
public boolean canAddEnergy() {
return this.status == RocketStatusEnum.CHARGING;
}
/**
* 是否可以领取
*/
public boolean canClaim() {
return this.status == RocketStatusEnum.CLAIMING
&& this.claimExpireTime != null
&& LocalDateTime.now().isBefore(this.claimExpireTime);
}
/**
* 是否已过期
*/
public boolean isExpired() {
return this.claimExpireTime != null
&& LocalDateTime.now().isAfter(this.claimExpireTime);
}
/**
* 获取能量百分比
*/
public BigDecimal getEnergyPercent() {
if (this.maxEnergy == null || this.maxEnergy == 0) {
return BigDecimal.ZERO;
}
return BigDecimal.valueOf(this.currentEnergy)
.divide(BigDecimal.valueOf(this.maxEnergy), 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100));
}
/**
* 查找贡献者
*/
private RocketContributor findContributor(Long userId) {
if (this.contributors == null) {
this.contributors = new ArrayList<>();
return null;
}
return this.contributors.stream()
.filter(c -> c.getUserId().equals(userId))
.findFirst()
.orElse(null);
}
/**
* 重新计算贡献率
*/
private void recalculateContributionRate() {
if (this.currentEnergy == null || this.currentEnergy == 0) {
return;
}
this.contributors.forEach(contributor -> {
BigDecimal rate = BigDecimal.valueOf(contributor.getEnergy())
.divide(BigDecimal.valueOf(this.currentEnergy), 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100));
contributor.setContributionRate(rate);
});
// 按贡献能量排序
this.contributors.sort(Comparator.comparing(RocketContributor::getEnergy).reversed());
}
/**
* 检查并升级等级
*/
private void checkAndUpgradeLevel() {
RocketLevel newLevel = RocketLevel.determineLevel(this.currentEnergy);
if (newLevel != this.level) {
this.level = newLevel;
this.maxEnergy = newLevel.getMaxEnergy();
}
}
}

View File

@ -0,0 +1,52 @@
package com.red.circle.other.domain.rocket;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 火箭状态枚举
*
* @author system
* @date 2025-01-15
*/
@Getter
@AllArgsConstructor
public enum RocketStatusEnum {
/**
* 充能中
*/
CHARGING(1, "充能中"),
/**
* 已发射
*/
LAUNCHED(2, "已发射"),
/**
* 领取中
*/
CLAIMING(3, "领取中");
/**
* 状态码
*/
private final Integer code;
/**
* 状态描述
*/
private final String desc;
/**
* 根据状态码获取枚举
*/
public static RocketStatusEnum of(Integer code) {
for (RocketStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
throw new IllegalArgumentException("不支持的火箭状态: " + code);
}
}

View File

@ -0,0 +1,62 @@
package com.red.circle.other.infra.database.cache.key;
import com.red.circle.component.redis.RedisKeys;
/**
* 火箭系统Redis Key枚举
*
* @author system
* @date 2025-01-15
*/
public enum RocketKeys implements RedisKeys {
/**
* 火箭状态缓存
* KEY: ROCKET:STATUS:{roomId}
* VALUE: JSON(RocketStatusCO)
* TTL: 24h
*/
STATUS,
/**
* 火箭能量进度缓存
* KEY: ROCKET:ENERGY:{roomId}
* VALUE: String(currentEnergy/maxEnergy)
* TTL: 24h
*/
ENERGY,
/**
* 火箭分布式锁
* KEY: ROCKET:LOCK:{roomId}
* VALUE: timestamp
* TTL: 5s
*/
LOCK,
/**
* 火箭奖励已领取集合
* KEY: ROCKET:CLAIMED:{rocketHistoryId}
* VALUE: Set(userId)
* TTL: 2min
*/
CLAIMED,
/**
* 火箭配置缓存
* KEY: ROCKET:CONFIG:{level}
* VALUE: JSON(RocketConfigCO)
* TTL: 1h
*/
CONFIG;
@Override
public String businessPrefix() {
return "ROCKET";
}
@Override
public String businessKey() {
return this.name();
}
}

View File

@ -0,0 +1,109 @@
package com.red.circle.other.infra.database.mongo.document;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.LocalDateTime;
import java.util.List;
/**
* 火箭状态MongoDB文档
*
* @author system
* @date 2025-01-15
*/
@Data
@Document(collection = "room_rocket_status")
@CompoundIndex(name = "uk_room_date", def = "{'room_id': 1, 'date': 1}", unique = true)
public class RocketStatusDocument {
/**
* 主键ID
*/
@Id
private String id;
/**
* 房间ID
*/
@Indexed
private Long roomId;
/**
* 日期 (格式: 2025-01-15)
*/
@Indexed
private String date;
/**
* 等级
*/
private Integer level;
/**
* 当前能量值
*/
private Long currentEnergy;
/**
* 最大能量值
*/
private Long maxEnergy;
/**
* 状态 (1:CHARGING 2:LAUNCHED 3:CLAIMING)
*/
@Indexed
private Integer status;
/**
* 贡献者列表
*/
private List<ContributorDoc> contributors;
/**
* 发射时间
*/
private LocalDateTime launchTime;
/**
* 领取过期时间
*/
@Indexed
private LocalDateTime claimExpireTime;
/**
* 已领取人数
*/
private Integer claimCount;
/**
* 创建时间
*/
@Indexed
private LocalDateTime createdAt;
/**
* 更新时间
*/
@Indexed
private LocalDateTime updatedAt;
/**
* 贡献者内嵌文档
*/
@Data
public static class ContributorDoc {
private Long userId;
private String userName;
private String userAvatar;
private Long energy;
private Double contributionRate;
private Integer giftCount;
private LocalDateTime firstContributeTime;
private LocalDateTime lastContributeTime;
}
}

View File

@ -0,0 +1,38 @@
package com.red.circle.other.infra.database.mongo.repository;
import com.red.circle.other.infra.database.mongo.document.RocketStatusDocument;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* 火箭状态Repository
*
* @author system
* @date 2025-01-15
*/
@Repository
public interface RocketStatusRepository extends MongoRepository<RocketStatusDocument, String> {
/**
* 根据房间ID和日期查询
*/
Optional<RocketStatusDocument> findByRoomIdAndDate(Long roomId, String date);
/**
* 根据房间ID列表和日期查询
*/
List<RocketStatusDocument> findByRoomIdInAndDate(List<Long> roomIds, String date);
/**
* 查询所有处于充能状态的火箭
*/
List<RocketStatusDocument> findByStatusAndDate(Integer status, String date);
/**
* 删除指定日期之前的数据
*/
void deleteByDateBefore(String date);
}

View File

@ -0,0 +1,154 @@
package com.red.circle.other.infra.gateway;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.domain.rocket.RocketStatusEnum;
import com.red.circle.other.infra.database.mongo.document.RocketStatusDocument;
import com.red.circle.other.infra.database.mongo.repository.RocketStatusRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;
/**
* 火箭状态Gateway实现
*
* @author system
* @date 2025-01-15
*/
@Component
@RequiredArgsConstructor
public class RocketStatusGatewayImpl implements RocketStatusGateway {
private final RocketStatusRepository rocketStatusRepository;
@Override
public RocketStatus findByRoomIdAndDate(Long roomId, String date) {
return rocketStatusRepository.findByRoomIdAndDate(roomId, date)
.map(this::toDomain)
.orElse(null);
}
@Override
public void save(RocketStatus rocketStatus) {
RocketStatusDocument document = toDocument(rocketStatus);
rocketStatusRepository.save(document);
// 回填ID
rocketStatus.setId(document.getId());
}
@Override
public List<RocketStatus> findByRoomIds(List<Long> roomIds, String date) {
List<RocketStatusDocument> documents = rocketStatusRepository.findByRoomIdInAndDate(roomIds, date);
return documents.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
@Override
public List<RocketStatus> findAllCharging(String date) {
List<RocketStatusDocument> documents = rocketStatusRepository.findByStatusAndDate(
RocketStatusEnum.CHARGING.getCode(),
date
);
return documents.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
@Override
public void delete(String id) {
rocketStatusRepository.deleteById(id);
}
/**
* Document转Domain
*/
private RocketStatus toDomain(RocketStatusDocument doc) {
RocketStatus status = new RocketStatus();
status.setId(doc.getId());
status.setRoomId(doc.getRoomId());
status.setDate(doc.getDate());
status.setLevel(com.red.circle.other.domain.rocket.RocketLevel.of(doc.getLevel()));
status.setCurrentEnergy(doc.getCurrentEnergy());
status.setMaxEnergy(doc.getMaxEnergy());
status.setStatus(RocketStatusEnum.of(doc.getStatus()));
// 转换贡献者
if (doc.getContributors() != null) {
List<com.red.circle.other.domain.rocket.RocketContributor> contributors = doc.getContributors().stream()
.map(c -> {
com.red.circle.other.domain.rocket.RocketContributor contributor =
new com.red.circle.other.domain.rocket.RocketContributor();
contributor.setUserId(c.getUserId());
contributor.setUserName(c.getUserName());
contributor.setUserAvatar(c.getUserAvatar());
contributor.setEnergy(c.getEnergy());
contributor.setContributionRate(
c.getContributionRate() != null ?
java.math.BigDecimal.valueOf(c.getContributionRate()) :
java.math.BigDecimal.ZERO
);
contributor.setGiftCount(c.getGiftCount());
contributor.setFirstContributeTime(c.getFirstContributeTime());
contributor.setLastContributeTime(c.getLastContributeTime());
return contributor;
})
.collect(Collectors.toList());
status.setContributors(contributors);
}
status.setLaunchTime(doc.getLaunchTime());
status.setClaimExpireTime(doc.getClaimExpireTime());
status.setClaimCount(doc.getClaimCount());
status.setCreatedAt(doc.getCreatedAt());
status.setUpdatedAt(doc.getUpdatedAt());
return status;
}
/**
* Domain转Document
*/
private RocketStatusDocument toDocument(RocketStatus status) {
RocketStatusDocument doc = new RocketStatusDocument();
doc.setId(status.getId());
doc.setRoomId(status.getRoomId());
doc.setDate(status.getDate() != null ? status.getDate() : LocalDate.now().toString());
doc.setLevel(status.getLevel().getLevel());
doc.setCurrentEnergy(status.getCurrentEnergy());
doc.setMaxEnergy(status.getMaxEnergy());
doc.setStatus(status.getStatus().getCode());
// 转换贡献者
if (status.getContributors() != null) {
List<RocketStatusDocument.ContributorDoc> contributors = status.getContributors().stream()
.map(c -> {
RocketStatusDocument.ContributorDoc doc1 = new RocketStatusDocument.ContributorDoc();
doc1.setUserId(c.getUserId());
doc1.setUserName(c.getUserName());
doc1.setUserAvatar(c.getUserAvatar());
doc1.setEnergy(c.getEnergy());
doc1.setContributionRate(
c.getContributionRate() != null ?
c.getContributionRate().doubleValue() :
0.0
);
doc1.setGiftCount(c.getGiftCount());
doc1.setFirstContributeTime(c.getFirstContributeTime());
doc1.setLastContributeTime(c.getLastContributeTime());
return doc1;
})
.collect(Collectors.toList());
doc.setContributors(contributors);
}
doc.setLaunchTime(status.getLaunchTime());
doc.setClaimExpireTime(status.getClaimExpireTime());
doc.setClaimCount(status.getClaimCount());
doc.setCreatedAt(status.getCreatedAt());
doc.setUpdatedAt(status.getUpdatedAt());
return doc;
}
}