新增火箭用户领取奖励

This commit is contained in:
tianfeng 2025-11-12 21:01:29 +08:00
parent 75c1e5522d
commit 3bdfc23433
14 changed files with 313 additions and 150 deletions

View File

@ -264,7 +264,12 @@ public enum SendPropsOrigin {
*/
LOTTERY_REWARD("Lottery Rewards"),
/**
/**
* 火箭奖励
*/
ROCKET_REWARD("Rocket reward"),
/**
* 道具券兑换
*/
COUPON_EXCHANGE("Coupon Exchange"),

View File

@ -703,6 +703,11 @@ public enum GoldOrigin {
*/
LOTTERY_REWARD("Lottery reward"),
/**
* 火箭奖励
*/
ROCKET_REWARD("Rocket reward"),
/**
* 抽奖兑换金币
*/

View File

@ -39,6 +39,11 @@ public class RocketConfigCO {
* 奖励配置(JSON)
*/
private JSONArray rewardConfig;
/**
* 奖励礼物数量
*/
private Integer rewardQuantity;
/**
* 贡献者奖励比例%

View File

@ -46,7 +46,7 @@ public class RocketRestController {
/**
* 领取奖励
*/
// @PostMapping("/claim")
@PostMapping("/claim")
public RocketRewardCO claimReward(@Valid @RequestBody RocketRewardClaimCmd cmd) {
return rocketService.claimReward(cmd);
}

View File

@ -1,22 +1,38 @@
package com.red.circle.other.app.command.rocket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.common.business.enums.SendPropsOrigin;
import com.red.circle.other.app.dto.clientobject.RocketRewardCO;
import com.red.circle.other.app.dto.cmd.RocketRewardClaimCmd;
import com.red.circle.other.app.manager.RocketImPushManager;
import com.red.circle.other.app.util.DistributedLockUtil;
import com.red.circle.other.domain.gateway.RocketConfigGateway;
import com.red.circle.other.domain.gateway.RocketHistoryGateway;
import com.red.circle.other.domain.gateway.RocketRewardLogGateway;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketContributor;
import com.red.circle.other.domain.rocket.RocketRewardLog;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.domain.rocket.*;
import com.red.circle.other.infra.database.cache.key.RocketKeys;
import com.red.circle.other.inner.endpoint.material.props.BadgeBackpackClient;
import com.red.circle.other.inner.endpoint.material.props.PropsBackpackClient;
import com.red.circle.other.inner.enums.material.BadgeBackpackExpireTypeEnum;
import com.red.circle.other.inner.enums.sys.SysBadgeConfigTypeEnum;
import com.red.circle.other.inner.model.cmd.material.GiveBadgeCmd;
import com.red.circle.other.inner.model.cmd.material.GivePropsBackpackCmd;
import com.red.circle.other.inner.model.dto.material.PropsActivityRewardConfigInfoDTO;
import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient;
import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd;
import com.red.circle.wallet.inner.model.enums.GoldOrigin;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Random;
/**
* 火箭奖励领取命令执行器
@ -30,113 +46,276 @@ import java.time.LocalDateTime;
public class RocketRewardClaimCmdExe {
private final RocketStatusGateway rocketStatusGateway;
private final RocketConfigGateway rocketConfigGateway;
private final RocketRewardLogGateway rocketRewardLogGateway;
private final RocketHistoryGateway rocketHistoryGateway;
private final DistributedLockUtil distributedLockUtil;
private final RocketImPushManager rocketImPushManager;
private final WalletGoldClient walletGoldClient;
private final PropsBackpackClient propsBackpackClient;
private final BadgeBackpackClient badgeBackpackClient;
// TODO: 注入钱包服务Feign Client
// private final WalletGoldClient walletGoldClient;
private final Random random = new Random();
/**
* 执行奖励领取
*/
public RocketRewardCO execute(RocketRewardClaimCmd cmd) {
// 构建锁Key (防止同一用户并发领取)
String lockKey = RocketKeys.LOCK.getKey(cmd.getRoomId(), "claim", cmd.getUserId());
Long userId = cmd.getReqUserId();
String lockKey = RocketKeys.LOCK.getKey(cmd.getRoomId(), "claim", userId);
return distributedLockUtil.executeWithLock(lockKey, 3L, () -> {
return doClaim(cmd);
}, "奖励领取操作过于频繁,请稍后重试");
return distributedLockUtil.executeWithLock(lockKey, 5L, () -> doClaim(cmd), "奖励领取操作过于频繁,请稍后重试");
}
/**
* 执行领取逻辑
*/
private RocketRewardCO doClaim(RocketRewardClaimCmd cmd) {
// 1. 查询火箭状态
Long userId = cmd.getReqUserId();
Long roomId = cmd.getRoomId();
String today = LocalDate.now().toString();
RocketStatus rocketStatus = rocketStatusGateway.findByRoomIdAndDate(cmd.getRoomId(), today);
// 1. 查询火箭状态
RocketStatus rocketStatus = rocketStatusGateway.findByRoomIdAndDate(roomId, today);
if (rocketStatus == null) {
throw new RuntimeException("火箭状态不存在");
}
// 2. 校验领取资格
if (!rocketStatus.canClaim()) {
if (rocketStatus.isExpired()) {
throw new RuntimeException("奖励领取已过期");
}
throw new RuntimeException("当前状态不可领取奖励");
Integer rocketLevel = rocketStatus.getLevel().getLevel();
// 2. 校验是否已发射
if (rocketStatus.getStatus() != RocketStatusEnum.LAUNCHED) {
throw new RuntimeException("火箭尚未发射,无法领取奖励");
}
// 3. 检查是否已领取 (MySQL防重)
boolean hasClaimed = rocketRewardLogGateway.existsByRocketAndUser(
cmd.getRocketHistoryId(), cmd.getUserId());
// 3. 检查是否已领取每天每个房间每个等级只能领一次
boolean hasClaimed = rocketRewardLogGateway.existsByRoomAndUserAndDateAndLevel(roomId, userId, today, rocketLevel);
if (hasClaimed) {
throw new RuntimeException("您已经领取过奖励了");
throw new RuntimeException("今日该等级火箭已领取,请等待下一级或明天再来");
}
// 4. 判断是否为贡献者
RocketContributor contributor = findContributor(rocketStatus, cmd.getUserId());
boolean isContributor = contributor != null;
// 4. 查询火箭配置
RocketConfig config = rocketConfigGateway.findByLevel(rocketLevel);
if (config == null) {
throw new RuntimeException("火箭配置不存在");
}
// 5. 计算奖励 (TODO: 后续根据配置计算)
Long rewardAmount = calculateReward(isContributor, contributor, rocketStatus);
// 5. 检查奖励数量是否已达上限
Integer rewardQuantity = config.getRewardQuantity() != null ? config.getRewardQuantity() : 20;
if (rocketStatus.getClaimCount() >= rewardQuantity) {
throw new RuntimeException("奖励已被领完,请下次早点来");
}
// 6. 解析奖励配置
List<PropsActivityRewardConfigInfoDTO> rewardList = parseRewardConfig(config.getRewardConfig().toJSONString());
if (rewardList == null || rewardList.isEmpty()) {
throw new RuntimeException("奖励配置为空");
}
// 7. 随机选择一个奖励
PropsActivityRewardConfigInfoDTO selectedReward = rewardList.get(random.nextInt(rewardList.size()));
RocketRewardCO rewardCO = new RocketRewardCO();
rewardCO.setIsContributor(isContributor);
rewardCO.setRewardType("GOLD");
rewardCO.setRewardAmount(rewardAmount);
if (isContributor) {
rewardCO.setContributionEnergy(contributor.getEnergy());
rewardCO.setContributionRate(contributor.getContributionRate());
rewardCO.setMessage("恭喜您获得贡献者奖励!");
} else {
rewardCO.setMessage("恭喜您获得参与奖励!");
}
// 8. 发放奖励
String walletBizNo = RocketRewardLog.buildBizNo(roomId.toString(), userId);
grantReward(userId, selectedReward, walletBizNo);
// 6. 发放奖励 (调用钱包服务)
String walletBizNo = RocketRewardLog.buildBizNo(cmd.getRocketHistoryId(), cmd.getUserId());
try {
sendRewardToWallet(cmd.getUserId(), rewardAmount, walletBizNo);
} catch (Exception e) {
log.error("调用钱包服务发放奖励失败: userId={}, amount={}", cmd.getUserId(), rewardAmount, e);
throw new RuntimeException("奖励发放失败,请稍后重试");
}
// 9. 保存领取记录
saveRewardLog(roomId, userId, today, rocketLevel, selectedReward, walletBizNo);
// 7. 记录领取日志 (MySQL)
saveRewardLog(cmd, rocketStatus, isContributor, contributor, rewardAmount, walletBizNo);
// 8. 更新领取人数
// 10. 更新领取人数
rocketStatus.incrementClaimCount();
rocketStatusGateway.save(rocketStatus);
// 9. 更新历史记录的领取统计
// 11. 推送领取成功消息
try {
rocketHistoryGateway.updateClaimStats(cmd.getRocketHistoryId(), 1, rewardAmount);
String userName = "用户" + userId;
Long rewardAmount = getRewardAmount(selectedReward);
rocketImPushManager.pushRewardClaimed(roomId, userId, userName, rewardAmount);
} catch (Exception e) {
log.error("更新历史记录领取统计失败: historyId={}", cmd.getRocketHistoryId(), e);
log.error("推送奖励领取消息失败: roomId={}, userId={}", roomId, userId, e);
}
// 10. 推送领取成功消息
try {
// TODO: 获取用户名称
String userName = "用户" + cmd.getUserId();
rocketImPushManager.pushRewardClaimed(cmd.getRoomId(), cmd.getUserId(), userName, rewardAmount);
} catch (Exception e) {
log.error("推送奖励领取消息失败: roomId={}, userId={}", cmd.getRoomId(), cmd.getUserId(), e);
}
// 12. 构建返回结果
RocketRewardCO rewardCO = new RocketRewardCO();
rewardCO.setRewardType(selectedReward.getType());
rewardCO.setRewardItemId(selectedReward.getId());
rewardCO.setRewardItemName(selectedReward.getName());
rewardCO.setRewardAmount(getRewardAmount(selectedReward));
rewardCO.setMessage("恭喜您获得火箭奖励!");
log.info("火箭奖励领取成功, roomId={}, userId={}, isContributor={}, rewardAmount={}",
cmd.getRoomId(), cmd.getUserId(), isContributor, rewardAmount);
log.info("火箭奖励领取成功: roomId={}, userId={}, level={}, rewardType={}, rewardId={}",
roomId, userId, rocketLevel, selectedReward.getType(), selectedReward.getId());
return rewardCO;
}
/**
* 查找贡献者
* 解析奖励配置
*/
private List<PropsActivityRewardConfigInfoDTO> parseRewardConfig(String rewardConfigJson) {
try {
JSONArray jsonArray = JSON.parseArray(rewardConfigJson);
return jsonArray.toJavaList(PropsActivityRewardConfigInfoDTO.class);
} catch (Exception e) {
log.error("解析奖励配置失败", e);
return null;
}
}
/**
* 发放奖励手动实现
*/
private void grantReward(Long userId, PropsActivityRewardConfigInfoDTO reward, String bizNo) {
try {
String rewardType = reward.getType();
switch (rewardType) {
case "GOLD" -> {
// 发放金币
Long goldAmount = Long.parseLong(reward.getContent());
grantGold(userId, goldAmount, bizNo);
}
case "PROPS" -> {
// 发放道具头饰/座驾等
Long propId = Long.parseLong(reward.getContent());
Integer quantity = reward.getQuantity() != null ? reward.getQuantity() : 1;
String detailType = reward.getDetailType(); // AVATAR_FRAME / RIDE
grantProp(userId, propId, quantity, detailType, bizNo);
}
case "BADGE" -> {
// 发放徽章
Long badgeId = Long.parseLong(reward.getContent());
Integer quantity = reward.getQuantity() != null ? reward.getQuantity() : 1;
grantBadge(userId, badgeId, quantity, bizNo);
}
default -> throw new RuntimeException("不支持的奖励类型: " + rewardType);
}
log.info("奖励发放成功: userId={}, type={}, bizNo={}", userId, rewardType, bizNo);
} catch (Exception e) {
log.error("发放奖励失败: userId={}, reward={}", userId, reward, e);
throw new RuntimeException("发放奖励失败: " + e.getMessage());
}
}
/**
* 发放金币
*/
private void grantGold(Long userId, Long goldAmount, String bizNo) {
try {
// 防止配错导致发送大量金币
BigDecimal amount = BigDecimal.valueOf(goldAmount);
walletGoldClient.changeBalance(
GoldReceiptCmd.builder()
.opsIncome()
.userId(userId)
.eventId(bizNo)
.sysOrigin(SysOriginPlatformEnum.LIKEI)
.origin(GoldOrigin.ROCKET_REWARD)
.originDescribe(GoldOrigin.ROCKET_REWARD.name())
.amount(amount)
.remark("")
.build()
);
log.info("金币发放成功: userId={}, amount={}", userId, goldAmount);
} catch (Exception e) {
log.error("金币发放失败: userId={}, amount={}", userId, goldAmount, e);
throw new RuntimeException("金币发放失败");
}
}
/**
* 发放道具头饰/座驾等
*/
private void grantProp(Long userId, Long propId, Integer quantity, String detailType, String bizNo) {
try {
GivePropsBackpackCmd cmd = new GivePropsBackpackCmd()
.setAcceptUserId(userId)
.setPropsId(propId)
.setType("PROPS")
.setOrigin(SendPropsOrigin.ROCKET_REWARD.name())
.setOriginDesc(SendPropsOrigin.ROCKET_REWARD.getDesc())
.setDays(quantity)
.setUseProps(Boolean.FALSE)
.setAllowGive(Boolean.FALSE)
.setOpUser(userId);
propsBackpackClient.giveProps(cmd);
log.info("道具发放成功(TODO): userId={}, propId={}, quantity={}, type={}",
userId, propId, quantity, detailType);
} catch (Exception e) {
log.error("道具发放失败: userId={}, propId={}", userId, propId, e);
throw new RuntimeException("道具发放失败");
}
}
/**
* 发放徽章
*/
private void grantBadge(Long userId, Long badgeId, Integer quantity, String bizNo) {
try {
// 徽章赠送天数大于1000天则为永久赠送
BadgeBackpackExpireTypeEnum expireType = quantity >= 1000
? BadgeBackpackExpireTypeEnum.PERMANENT
: BadgeBackpackExpireTypeEnum.TEMPORARY;
badgeBackpackClient.giveBadge(new GiveBadgeCmd()
.setAcceptUserId(userId)
.setBadgeId(badgeId)
.setExpireType(expireType)
.setDays(quantity)
.setBadgeType(SysBadgeConfigTypeEnum.ACTIVITY)
);
log.info("徽章发放成功(TODO): userId={}, badgeId={}", userId, badgeId);
} catch (Exception e) {
log.error("徽章发放失败: userId={}, badgeId={}", userId, badgeId, e);
throw new RuntimeException("徽章发放失败");
}
}
/**
* 保存领取记录
*/
private void saveRewardLog(Long roomId, Long userId, String date, Integer level,
PropsActivityRewardConfigInfoDTO reward, String walletBizNo) {
RocketRewardLog log = new RocketRewardLog();
log.setRoomId(roomId);
log.setUserId(userId);
log.setRocketLevel(level);
log.setIsContributor(0);
log.setContributionEnergy(0L);
log.setContributionRate(null);
log.setRewardType(reward.getType());
log.setRewardItemId(reward.getId());
log.setRewardAmount(getRewardAmount(reward));
log.setWalletBizNo(walletBizNo);
log.setClaimedAt(LocalDateTime.now());
log.setCreatedAt(LocalDateTime.now());
rocketRewardLogGateway.save(log);
}
/**
* 获取奖励金额
*/
private Long getRewardAmount(PropsActivityRewardConfigInfoDTO reward) {
if ("GOLD".equals(reward.getType())) {
try {
return Long.parseLong(reward.getContent());
} catch (Exception e) {
return 0L;
}
}
int quantity = reward.getQuantity() == null ? 0 : reward.getQuantity();
return Long.parseLong(Integer.toString(quantity));
}
/**
* 查找贡献者备用
*/
private RocketContributor findContributor(RocketStatus rocketStatus, Long userId) {
if (rocketStatus.getContributors() == null) {
@ -147,73 +326,4 @@ public class RocketRewardClaimCmdExe {
.findFirst()
.orElse(null);
}
/**
* 计算奖励金额
* TODO: 后续根据配置表计算
*/
private Long calculateReward(boolean isContributor, RocketContributor contributor, RocketStatus rocketStatus) {
if (isContributor) {
// 贡献者奖励 = 基础奖励 + 按贡献率分配的额外奖励
// 目前固定返回1000后续根据配置计算
return 1000L;
} else {
// 普通用户固定奖励
return 100L;
}
}
/**
* 调用钱包服务发放奖励
* TODO: 集成wallet-inner-api
*/
private void sendRewardToWallet(Long userId, Long amount, String bizNo) {
// 示例代码需要引入实际的Feign Client
/*
WalletReceiptReqDTO req = new WalletReceiptReqDTO();
req.setUserId(userId);
req.setAmount(PennyAmount.of(amount)); // 金币
req.setBizType(WalletBizType.ROCKET_REWARD);
req.setBizNo(bizNo);
req.setRemark("火箭奖励");
ResultResponse<Void> response = walletGoldClient.income(req);
if (!response.isSuccess()) {
throw new RuntimeException("钱包服务调用失败: " + response.getMessage());
}
*/
log.info("TODO: 调用钱包服务发放奖励, userId={}, amount={}, bizNo={}", userId, amount, bizNo);
}
/**
* 保存领取记录
*/
private void saveRewardLog(RocketRewardClaimCmd cmd, RocketStatus rocketStatus,
boolean isContributor, RocketContributor contributor,
Long rewardAmount, String walletBizNo) {
RocketRewardLog log = new RocketRewardLog();
log.setRocketHistoryId(cmd.getRocketHistoryId());
log.setRoomId(cmd.getRoomId());
log.setUserId(cmd.getUserId());
log.setRocketLevel(rocketStatus.getLevel().getLevel());
log.setIsContributor(isContributor ? 1 : 0);
if (isContributor && contributor != null) {
log.setContributionEnergy(contributor.getEnergy());
log.setContributionRate(contributor.getContributionRate());
} else {
log.setContributionEnergy(0L);
log.setContributionRate(null);
}
log.setRewardType("GOLD");
log.setRewardItemId(null);
log.setRewardAmount(rewardAmount);
log.setWalletBizNo(walletBizNo);
log.setClaimedAt(LocalDateTime.now());
log.setCreatedAt(LocalDateTime.now());
rocketRewardLogGateway.save(log);
}
}

View File

@ -23,6 +23,11 @@ public class RocketRewardCO {
*/
private Long rewardItemId;
/**
* 奖励物品名称
*/
private String rewardItemName;
/**
* 奖励数量
*/

View File

@ -1,8 +1,10 @@
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;
/**
* 火箭奖励领取命令
@ -10,8 +12,9 @@ import lombok.Data;
* @author system
* @date 2025-01-15
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class RocketRewardClaimCmd {
public class RocketRewardClaimCmd extends AppExtCommand {
/**
* 房间ID
@ -19,15 +22,4 @@ public class RocketRewardClaimCmd {
@NotNull(message = "房间ID不能为空")
private Long roomId;
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空")
private Long userId;
/**
* 火箭历史ID (MongoDB _id)
*/
@NotBlank(message = "火箭历史ID不能为空")
private String rocketHistoryId;
}

View File

@ -22,6 +22,16 @@ public interface RocketRewardLogGateway {
*/
boolean existsByRocketAndUser(String rocketHistoryId, Long userId);
/**
* 检查用户是否已领取带等级
* @param roomId 房间ID
* @param userId 用户ID
* @param date 日期
* @param level 火箭等级
* @return 是否已领取
*/
boolean existsByRoomAndUserAndDateAndLevel(Long roomId, Long userId, String date, Integer level);
/**
* 根据火箭历史ID查询所有领取记录
*/

View File

@ -36,6 +36,11 @@ public class RocketConfig {
*/
private JSONArray rewardConfig;
/**
* 奖励礼物数量默认20个
*/
private Integer rewardQuantity;
/**
* 贡献者奖励比例%
*/

View File

@ -25,6 +25,5 @@ public interface RocketRewardLogConvertor {
/**
* Domain转Entity
*/
@Mapping(target = "createdAt", ignore = true)
RocketRewardClaimLogEntity toEntity(RocketRewardLog log);
}

View File

@ -24,6 +24,14 @@ public interface RocketRewardClaimLogDAO {
int countByRocketAndUser(@Param("rocketHistoryId") String rocketHistoryId,
@Param("userId") Long userId);
/**
* 检查用户是否已领取带等级
*/
int countByRoomAndUserAndDateAndLevel(@Param("userId") Long userId,
@Param("roomId") Long roomId,
@Param("date") String date,
@Param("level") Integer level);
/**
* 根据火箭历史ID查询
*/

View File

@ -36,6 +36,11 @@ public class RocketConfigEntity {
*/
private String rewardConfig;
/**
* 奖励礼物数量默认20个
*/
private Integer rewardQuantity;
/**
* 贡献者奖励比例%
*/

View File

@ -39,6 +39,12 @@ public class RocketRewardLogGatewayImpl implements RocketRewardLogGateway {
return count > 0;
}
@Override
public boolean existsByRoomAndUserAndDateAndLevel(Long roomId, Long userId, String date, Integer level) {
int count = rocketRewardClaimLogDAO.countByRoomAndUserAndDateAndLevel(userId, roomId, date, level);
return count > 0;
}
@Override
public List<RocketRewardLog> findByRocketHistoryId(String rocketHistoryId) {
List<RocketRewardClaimLogEntity> entities =

View File

@ -68,4 +68,12 @@
WHERE rocket_history_id = #{rocketHistoryId}
</select>
<select id="countByRoomAndUserAndDateAndLevel" resultType="int">
SELECT COUNT(1)
FROM rocket_reward_claim_log
WHERE user_id = #{userId}
AND room_id = #{roomId}
AND DATE(claimed_at) = #{date}
AND rocket_level = #{level}
</select>
</mapper>