游戏王活动

This commit is contained in:
tianfeng 2025-12-18 14:09:38 +08:00
parent 5cfa816752
commit 2c28ec5135
16 changed files with 914 additions and 58 deletions

View File

@ -8,8 +8,6 @@ import lombok.experimental.Accessors;
/**
* 徽章操作事件消息.
*
* @author system
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ -29,6 +27,16 @@ public class BadgeOperateEvent implements Serializable {
*/
private String badgeKey;
/**
* 徽章ID.
*/
private Long badgeId;
/**
* 天数.
*/
private Long days;
/**
* 操作类型: WEAR-佩戴, REMOVE-移除.
*/

View File

@ -0,0 +1,62 @@
package com.red.circle.mq.business.model.event;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 游戏王奖励事件.
*/
@Data
@Accessors(chain = true)
public class GameKingRewardEvent {
/**
* 活动类型.
*/
private String activityType;
/**
* 来源系统.
*/
private String origin;
/**
* 周期Key.
*/
private String cycleKey;
/**
* 模板ID.
*/
private String templateId;
/**
* 排名数据.
*/
private List<RankingUser> rankingUsers;
/**
* 排名用户信息.
*/
@Data
@Accessors(chain = true)
public static class RankingUser {
/**
* 用户ID.
*/
private Long userId;
/**
* 排名.
*/
private Integer rank;
/**
* 分数.
*/
private Long score;
}
}

View File

@ -152,6 +152,11 @@ public interface QueueNameConstant {
*/
String USER_HEARTBEAT = "USER_HEARTBEAT";
/**
* 游戏王奖励.
*/
String GAME_KING_REWARD = "GAME_KING_REWARD";
// =================================================后台管理=============================================================
/**
* 后台管理审批.

View File

@ -5,6 +5,7 @@ import com.red.circle.component.mq.service.FixedTopic;
import com.red.circle.component.mq.service.SenderService;
import com.red.circle.mq.business.model.event.BadgeEvent;
import com.red.circle.mq.business.model.event.BadgeOperateEvent;
import com.red.circle.mq.business.model.event.GameKingRewardEvent;
import com.red.circle.mq.business.model.event.user.AccountBeautifulNumberEvent;
import com.red.circle.mq.business.model.event.user.CpApplyEvent;
import com.red.circle.mq.business.model.event.user.LoginLoggerEvent;
@ -15,6 +16,7 @@ import com.red.circle.mq.rocket.business.streams.AccountBeautifulNumberSink;
import com.red.circle.mq.rocket.business.streams.BadgeCheckSink;
import com.red.circle.mq.rocket.business.streams.BadgeOperateSink;
import com.red.circle.mq.rocket.business.streams.CpApplySink;
import com.red.circle.mq.rocket.business.streams.GameKingRewardSink;
import com.red.circle.mq.rocket.business.streams.LoginRegisterLoggerSink;
import com.red.circle.mq.rocket.business.streams.OnlineStatusUploadSink;
import com.red.circle.mq.rocket.business.streams.UserCardApplySink;
@ -26,8 +28,6 @@ import org.springframework.stereotype.Component;
/**
* 用户相关队列消息.
*
* @author pengliang on 2023/5/22
*/
@Component
@RequiredArgsConstructor
@ -126,11 +126,7 @@ public class UserMqMessageService {
}
/**
* 发送徽章操作事件.
*
* @param userId 用户ID
* @param badgeKey 徽章Key
* @param operationType 操作类型: WEAR-佩戴, REMOVE-移除
* 发送徽章操作事件 (旧版本兼容 - 通过badgeKey).
*/
public void sendBadgeOperateEvent(Long userId, String badgeKey, String operationType) {
senderService.sendEventMessage(MessageEvent.builder()
@ -145,4 +141,28 @@ public class UserMqMessageService {
.build());
}
/**
* 发送徽章操作事件 (新版本 - 传完整事件对象).
*/
public void sendBadgeOperateEvent(BadgeOperateEvent event) {
senderService.sendEventMessage(MessageEvent.builder()
.consumeId(IdWorkerUtils.getIdStr())
.topic(FixedTopic.RC_DEFAULT_APP_ORDINARY)
.tag(BadgeOperateSink.TAG)
.body(event)
.build());
}
/**
* 发送游戏王奖励事件.
*/
public void sendGameKingRewardEvent(GameKingRewardEvent event) {
senderService.sendEventMessage(MessageEvent.builder()
.consumeId(IdWorkerUtils.getIdStr())
.topic(FixedTopic.RC_DEFAULT_APP_ORDINARY)
.tag(GameKingRewardSink.TAG)
.body(event)
.build());
}
}

View File

@ -0,0 +1,15 @@
package com.red.circle.mq.rocket.business.streams;
import com.red.circle.mq.rocket.business.constant.QueueNameConstant;
/**
* 游戏王奖励.
*
* @author system
*/
public interface GameKingRewardSink {
String INPUT = QueueNameConstant.GAME_KING_REWARD;
String TAG = "game_king_reward_v1";
}

View File

@ -3,18 +3,22 @@ package com.red.circle.other.adapter.app.activity;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.other.app.dto.clientobject.activity.HistoricalTop1ListCO;
import com.red.circle.other.app.dto.clientobject.activity.RankingListCO;
import com.red.circle.other.app.dto.clientobject.activity.RankingUserVO;
import com.red.circle.other.app.dto.clientobject.activity.UserRankInfoCO;
import com.red.circle.other.app.dto.cmd.UserRankQueryCmd;
import com.red.circle.other.app.dto.cmd.activity.HistoricalTop1QueryCmd;
import com.red.circle.other.app.service.activity.RankingActivityService;
import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingListQueryCmd;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 排行榜活动控制器
*/
@ -67,4 +71,22 @@ public class RankingActivityRestController extends BaseController {
public HistoricalTop1ListCO getHistoricalTop1List(@RequestBody @Validated HistoricalTop1QueryCmd cmd) {
return rankingActivityService.getHistoricalTop1List(cmd);
}
/**
* 查询排行榜前四名带缓存和奖励发送
*
* 用途获取指定活动的前四名排行榜数据缓存五分钟查询DB时发送游戏王奖励消息
*
* @param cmd 查询命令
* @return 排行榜前四名
*/
@PostMapping("/top-four-with-reward")
public List<RankingUserVO> getTopFourRankingWithReward(@RequestBody RankingListQueryCmd cmd) {
if (cmd.getCycleType() == null) {
cmd.setCycleKey(ZonedDateTimeAsiaRiyadhUtils.nowWeekMondayToInt().toString());
}
cmd.setActivityType(RankingActivityType.KING_GAMES.getCode());
cmd.setTemplateId("2001207026499137537");
return rankingActivityService.getTopFourRankingWithReward(cmd);
}
}

View File

@ -0,0 +1,295 @@
package com.red.circle.other.app.command.activity;
import com.google.common.collect.Sets;
import com.red.circle.common.business.enums.SendPropsOrigin;
import com.red.circle.mq.business.model.event.BadgeOperateEvent;
import com.red.circle.mq.rocket.business.producer.UserMqMessageService;
import com.red.circle.other.app.dto.clientobject.sys.ActivityConfigVO;
import com.red.circle.other.infra.database.mongo.entity.ranking.RankingRewardSnapshot;
import com.red.circle.other.infra.database.mongo.service.ranking.RankingRewardSnapshotService;
import com.red.circle.other.infra.database.rds.entity.badge.BadgeConfig;
import com.red.circle.other.infra.database.rds.service.badge.BadgeConfigService;
import com.red.circle.other.inner.endpoint.material.props.PropsBackpackClient;
import com.red.circle.other.inner.enums.material.PropsCommodityType;
import com.red.circle.other.inner.model.cmd.material.DeletePropsCmd;
import com.red.circle.other.inner.model.cmd.material.GivePropsBackpackCmd;
import com.red.circle.other.inner.model.dto.material.PropsActivityRewardConfigInfoDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 排名奖励发放执行器.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RankingRewardGrantCmdExe {
private final RankingRewardSnapshotService snapshotService;
private final PropsBackpackClient propsBackpackClient;
private final UserMqMessageService userMqMessageService;
private final BadgeConfigService badgeConfigService;
/**
* 处理排名奖励变化.
*/
public void execute(String activityType, String origin, String cycleKey,
String templateId, List<RankingUserData> currentRankingUsers,
ActivityConfigVO activityConfig) {
RankingRewardSnapshot lastSnapshot = snapshotService.getLatestSnapshot(activityType, origin, cycleKey);
Map<Integer, RankingUserData> currentRankMap = currentRankingUsers.stream()
.collect(Collectors.toMap(RankingUserData::getRank, u -> u));
Map<Long, RankingRewardSnapshot.RankingUserSnapshot> lastUserMap = new HashMap<>();
if (lastSnapshot != null && CollectionUtils.isNotEmpty(lastSnapshot.getRankingUsers())) {
lastUserMap = lastSnapshot.getRankingUsers().stream()
.collect(Collectors.toMap(RankingRewardSnapshot.RankingUserSnapshot::getUserId, u -> u));
}
processRewardChanges(currentRankMap, lastUserMap, activityConfig);
saveNewSnapshot(activityType, origin, cycleKey, templateId, currentRankingUsers, activityConfig);
}
/**
* 处理奖励变化.
*/
private void processRewardChanges(Map<Integer, RankingUserData> currentRankMap,
Map<Long, RankingRewardSnapshot.RankingUserSnapshot> lastUserMap,
ActivityConfigVO activityConfig) {
Set<Long> currentUserIds = currentRankMap.values().stream()
.map(RankingUserData::getUserId)
.collect(Collectors.toSet());
for (RankingRewardSnapshot.RankingUserSnapshot lastUser : lastUserMap.values()) {
if (!currentUserIds.contains(lastUser.getUserId())) {
log.info("用户失去排名,收回奖励, userId: {}, rank: {}", lastUser.getUserId(), lastUser.getRank());
revokeRewards(lastUser.getUserId(), lastUser.getRewards());
}
}
for (RankingUserData currentUser : currentRankMap.values()) {
Long userId = currentUser.getUserId();
Integer currentRank = currentUser.getRank();
RankingRewardSnapshot.RankingUserSnapshot lastUser = lastUserMap.get(userId);
if (lastUser == null) {
log.info("用户新获得排名,发放奖励, userId: {}, rank: {}", userId, currentRank);
List<RankingRewardSnapshot.RewardItem> rewards = getRewardsForRank(currentRank, activityConfig);
grantRewards(userId, rewards);
} else if (!Objects.equals(lastUser.getRank(), currentRank)) {
log.info("用户排名变化, userId: {}, 旧排名: {}, 新排名: {}",
userId, lastUser.getRank(), currentRank);
revokeRewards(userId, lastUser.getRewards());
List<RankingRewardSnapshot.RewardItem> rewards = getRewardsForRank(currentRank, activityConfig);
grantRewards(userId, rewards);
}
}
}
/**
* 发放奖励.
*/
private void grantRewards(Long userId, List<RankingRewardSnapshot.RewardItem> rewards) {
if (CollectionUtils.isEmpty(rewards)) {
return;
}
for (RankingRewardSnapshot.RewardItem reward : rewards) {
try {
if ("PROPS".equals(reward.getType())) {
grantProps(userId, reward);
} else if ("BADGE".equals(reward.getType())) {
grantBadge(userId, reward);
}
} catch (Exception e) {
log.error("发放奖励失败, userId: {}, reward: {}", userId, reward, e);
}
}
}
/**
* 收回奖励.
*/
private void revokeRewards(Long userId, List<RankingRewardSnapshot.RewardItem> rewards) {
if (CollectionUtils.isEmpty(rewards)) {
return;
}
for (RankingRewardSnapshot.RewardItem reward : rewards) {
try {
if ("BADGE".equals(reward.getType())) {
revokeBadge(userId, reward);
} else if ("PROPS".equals(reward.getType())) {
revokeProps(userId, reward);
}
} catch (Exception e) {
log.error("收回奖励失败, userId: {}, reward: {}", userId, reward, e);
}
}
}
/**
* 发放道具.
*/
private void grantProps(Long userId, RankingRewardSnapshot.RewardItem reward) {
GivePropsBackpackCmd cmd = new GivePropsBackpackCmd()
.setAcceptUserId(userId)
.setPropsId(Long.valueOf(reward.getContent()))
.setType(reward.getDetailType())
.setOrigin(SendPropsOrigin.ACTIVITY_REWARD.name())
.setOriginDesc("游戏王排名奖励")
.setDays(reward.getQuantity())
.setUseProps(Boolean.TRUE)
.setAllowGive(Boolean.FALSE)
.setOpUser(0L);
propsBackpackClient.giveProps(cmd);
propsBackpackClient.givePropsNotify(userId, reward.getDetailType(),
reward.getQuantity(), Long.valueOf(reward.getContent()));
}
/**
* 收回道具.
*/
private void revokeProps(Long userId, RankingRewardSnapshot.RewardItem reward) {
DeletePropsCmd cmd = new DeletePropsCmd()
.setUserIds(Sets.newHashSet(userId))
.setPropsIds(Sets.newHashSet(Long.valueOf(reward.getContent())))
.setPropsType(PropsCommodityType.valueOf(reward.getDetailType()));
propsBackpackClient.deleteProps(cmd);
}
/**
* 发放徽章.
*/
private void grantBadge(Long userId, RankingRewardSnapshot.RewardItem reward) {
BadgeOperateEvent event = new BadgeOperateEvent()
.setUserId(userId)
.setBadgeId(Long.valueOf(reward.getContent()))
.setDays(1L)
.setOperationType(BadgeOperateEvent.OperationType.WEAR.name());
userMqMessageService.sendBadgeOperateEvent(event);
}
/**
* 收回徽章.
*/
private void revokeBadge(Long userId, RankingRewardSnapshot.RewardItem reward) {
BadgeOperateEvent event = new BadgeOperateEvent()
.setUserId(userId)
.setBadgeId(Long.valueOf(reward.getContent()))
.setDays(1L)
.setOperationType(BadgeOperateEvent.OperationType.REMOVE.name());
userMqMessageService.sendBadgeOperateEvent(event);
}
/**
* 获取指定排名的奖励列表.
*/
private List<RankingRewardSnapshot.RewardItem> getRewardsForRank(Integer rank, ActivityConfigVO activityConfig) {
if (activityConfig == null || CollectionUtils.isEmpty(activityConfig.getButOneRewards())) {
return Collections.emptyList();
}
String rankRange = getRankRange(rank);
return activityConfig.getButOneRewards().stream()
.filter(r -> rankRange.equals(r.getRankRange()))
.flatMap(r -> {
if (CollectionUtils.isEmpty(r.getRewards())) {
return java.util.stream.Stream.empty();
}
return r.getRewards().stream()
.filter(this::isSupportedRewardType)
.map(this::convertToRewardItem);
})
.collect(Collectors.toList());
}
/**
* 判断是否为支持的奖励类型.
*/
private boolean isSupportedRewardType(PropsActivityRewardConfigInfoDTO dto) {
return ("PROPS".equals(dto.getType()) && "AVATAR_FRAME".equals(dto.getDetailType())) ||
("BADGE".equals(dto.getType()) && "HONOR_ACTIVITY".equals(dto.getDetailType()));
}
/**
* 转换为奖励项.
*/
private RankingRewardSnapshot.RewardItem convertToRewardItem(PropsActivityRewardConfigInfoDTO dto) {
return new RankingRewardSnapshot.RewardItem()
.setType(dto.getType())
.setDetailType(dto.getDetailType())
.setContent(dto.getContent())
.setQuantity(dto.getQuantity())
.setBadgeName(dto.getBadgeName());
}
/**
* 获取排名范围.
*/
private String getRankRange(Integer rank) {
if (rank == 1) return "1";
if (rank == 2) return "2";
if (rank == 3) return "3";
if (rank <= 10) return "4-10";
return "other";
}
/**
* 保存新快照.
*/
private void saveNewSnapshot(String activityType, String origin, String cycleKey,
String templateId, List<RankingUserData> currentRankingUsers,
ActivityConfigVO activityConfig) {
List<RankingRewardSnapshot.RankingUserSnapshot> snapshotUsers = currentRankingUsers.stream()
.map(user -> {
String rankRange = getRankRange(user.getRank());
List<RankingRewardSnapshot.RewardItem> rewards = getRewardsForRank(user.getRank(), activityConfig);
return new RankingRewardSnapshot.RankingUserSnapshot()
.setUserId(user.getUserId())
.setRank(user.getRank())
.setRankRange(rankRange)
.setScore(user.getScore())
.setRewards(rewards);
})
.collect(Collectors.toList());
RankingRewardSnapshot snapshot = new RankingRewardSnapshot()
.setActivityType(activityType)
.setOrigin(origin)
.setCycleKey(cycleKey)
.setTemplateId(templateId)
.setSnapshotTime(LocalDateTime.now())
.setRankingUsers(snapshotUsers);
snapshotService.save(snapshot);
}
/**
* 排名用户数据.
*/
@lombok.Data
public static class RankingUserData {
private Long userId;
private Integer rank;
private Long score;
}
}

View File

@ -31,8 +31,6 @@ import lombok.extern.slf4j.Slf4j;
/**
* 徽章操作处理.
*
* @author system
*/
@Slf4j
@RocketMqMessageListener(groupId = BadgeOperateSink.INPUT, tag = BadgeOperateSink.TAG)
@ -63,28 +61,35 @@ public class BadgeOperateListener implements MessageListener {
private void handleBadgeOperate(BadgeOperateEvent event) {
Long userId = event.getUserId();
String badgeKey = event.getBadgeKey();
Long badgeId = event.getBadgeId();
String operationType = event.getOperationType();
if (userId == null || badgeKey == null || operationType == null) {
log.warn("徽章操作事件参数不完整, userId: {}, badgeKey: {}, operationType: {}",
userId, badgeKey, operationType);
if (userId == null || operationType == null) {
log.warn("徽章操作事件参数不完整, userId: {}, operationType: {}", userId, operationType);
return;
}
// 根据badgeKey查询徽章配置
BadgeConfig badgeConfig = badgeConfigService.getByBadgeKey(badgeKey);
if (badgeConfig == null) {
log.warn("徽章配置不存在, badgeKey: {}", badgeKey);
return;
Long finalBadgeId = badgeId;
// 如果传了badgeId则使用badgeId否则通过badgeKey查询
if (finalBadgeId == null) {
if (badgeKey == null) {
log.warn("徽章操作事件缺少badgeKey和badgeId, userId: {}", userId);
return;
}
BadgeConfig badgeConfig = badgeConfigService.getByBadgeKey(badgeKey);
if (badgeConfig == null) {
log.warn("徽章配置不存在, badgeKey: {}", badgeKey);
return;
}
finalBadgeId = badgeConfig.getId();
}
Long badgeId = badgeConfig.getId();
// 根据操作类型执行对应逻辑
if (BadgeOperateEvent.OperationType.WEAR.name().equals(operationType)) {
handleWearBadge(userId, badgeId);
handleWearBadge(userId, finalBadgeId, event.getDays());
} else if (BadgeOperateEvent.OperationType.REMOVE.name().equals(operationType)) {
handleRemoveBadge(userId, badgeId);
handleRemoveBadge(userId, finalBadgeId);
} else {
log.warn("未知的操作类型, operationType: {}", operationType);
}
@ -93,11 +98,10 @@ public class BadgeOperateListener implements MessageListener {
/**
* 佩戴徽章.
*/
private void handleWearBadge(Long userId, Long badgeId) {
private void handleWearBadge(Long userId, Long badgeId, Long days) {
String lockKey = "badge:operate:wear:" + userId + ":" + badgeId;
distributedLockUtil.executeWithLock(lockKey, () -> {
// 检查用户是否已经拥有该徽章幂等处理
BadgeBackpack existBadge = badgeBackpackService.query()
.eq(BadgeBackpack::getUserId, userId)
.eq(BadgeBackpack::getBadgeId, badgeId)
@ -110,7 +114,7 @@ public class BadgeOperateListener implements MessageListener {
BadgeBackpack badgeBackpack = badgeBackpackService.getByUserIdByBadgeId(userId, badgeId);
if (Objects.isNull(badgeBackpack)) {
badgeBackpackService.save(buildSaveUserBadgeBackpack(badgeId, userId));
badgeBackpackService.save(buildSaveUserBadgeBackpack(badgeId, userId, days));
userProfileGateway.removeCacheAll(userId);
}
return null;
@ -121,33 +125,37 @@ public class BadgeOperateListener implements MessageListener {
* 移除徽章.
*/
private void handleRemoveBadge(Long userId, Long badgeId) {
badgeBackpackService.deleteBadges(userId, Sets.newHashSet(badgeId));
userProfileGateway.removeCache(userId);
UserProfile userProfile = userProfileGateway.getByUserId(userId);
if (Objects.isNull(userProfile)) {
return;
}
List<UserUseBadgeDTO> wearBadge = userProfile.getWearBadge();
List<UserUseBadgeDTO> honorBadge = userProfile.getWearHonor();
wearBadge.addAll(honorBadge);
if (CollectionUtils.isEmpty(wearBadge)) {
return;
}
//过滤需要删除的 wearBadge id
Set<Long> saveWear = wearBadge.stream().map(UserUseBadgeDTO::getId)
.filter(id -> !badgeId.equals(id)).collect(Collectors.toSet());
userProfileGateway.updateUserWearBadge(userId, userProfile.getOriginSys(),saveWear);
badgeBackpackService.deleteBadges(userId, Sets.newHashSet(badgeId));
userProfileGateway.removeCache(userId);
UserProfile userProfile = userProfileGateway.getByUserId(userId);
if (Objects.isNull(userProfile)) {
return;
}
List<UserUseBadgeDTO> wearBadge = userProfile.getWearBadge();
List<UserUseBadgeDTO> honorBadge = userProfile.getWearHonor();
wearBadge.addAll(honorBadge);
if (CollectionUtils.isEmpty(wearBadge)) {
return;
}
Set<Long> saveWear = wearBadge.stream().map(UserUseBadgeDTO::getId)
.filter(id -> !badgeId.equals(id)).collect(Collectors.toSet());
userProfileGateway.updateUserWearBadge(userId, userProfile.getOriginSys(), saveWear);
}
private BadgeBackpack buildSaveUserBadgeBackpack(Long badgeId, Long userId, Long days) {
BadgeBackpack badgeBackpack = new BadgeBackpack()
.setUserId(userId)
.setBadgeId(badgeId)
.setUseProps(Boolean.TRUE);
private BadgeBackpack buildSaveUserBadgeBackpack(Long badgeId, Long userId) {
return new BadgeBackpack()
.setUserId(userId)
.setBadgeId(badgeId)
.setExpireTime(TimestampUtils.now())
.setExpireType(BadgeBackpackExpireTypeEnum.PERMANENT.name())
.setUseProps(Boolean.TRUE);
if (days != null && days > 0) {
badgeBackpack.setExpireTime(TimestampUtils.nowPlusDays(days.intValue()));
badgeBackpack.setExpireType(BadgeBackpackExpireTypeEnum.TEMPORARY.name());
} else {
badgeBackpack.setExpireTime(TimestampUtils.now());
badgeBackpack.setExpireType(BadgeBackpackExpireTypeEnum.PERMANENT.name());
}
return badgeBackpack;
}
}

View File

@ -0,0 +1,117 @@
package com.red.circle.other.app.listener;
import com.google.common.collect.Sets;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.component.mq.MessageEventProcess;
import com.red.circle.component.mq.MessageEventProcessDescribe;
import com.red.circle.component.mq.config.RocketMqMessageListener;
import com.red.circle.component.mq.service.Action;
import com.red.circle.component.mq.service.ConsumerMessage;
import com.red.circle.component.mq.service.MessageListener;
import com.red.circle.framework.core.dto.ReqSysOrigin;
import com.red.circle.mq.business.model.event.GameKingRewardEvent;
import com.red.circle.mq.rocket.business.streams.GameKingRewardSink;
import com.red.circle.other.app.command.activity.RankingRewardGrantCmdExe;
import com.red.circle.other.app.dto.clientobject.sys.ActivityConfigVO;
import com.red.circle.other.app.service.activity.WeekKingQueenService;
import com.red.circle.other.app.util.DistributedLockUtil;
import com.red.circle.tool.core.collection.CollectionUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.stream.Collectors;
/**
* 游戏王奖励监听器.
*/
@Slf4j
@RocketMqMessageListener(groupId = GameKingRewardSink.INPUT, tag = GameKingRewardSink.TAG)
@RequiredArgsConstructor
public class GameKingRewardListener implements MessageListener {
private final MessageEventProcess messageEventProcess;
private final DistributedLockUtil distributedLockUtil;
private final WeekKingQueenService weekKingQueenService;
private final RankingRewardGrantCmdExe rankingRewardGrantCmdExe;
@Override
public Action consume(ConsumerMessage message) {
return messageEventProcess.consume(
MessageEventProcessDescribe.builder()
.logTag("游戏王奖励处理")
.consumeMsgTimeoutMinute(60)
.repeatConsumeMinute(1)
.repeatConsumeTag("GameKingReward")
.checkConsumeTag(GameKingRewardSink.TAG)
.message(message)
.build(),
GameKingRewardEvent.class,
this::handleGameKingReward);
}
/**
* 处理游戏王奖励事件.
*/
private void handleGameKingReward(GameKingRewardEvent event) {
String activityType = event.getActivityType();
String origin = event.getOrigin();
String cycleKey = event.getCycleKey();
String templateId = event.getTemplateId() == null ? "-1" : event.getTemplateId();
if (activityType == null || origin == null || cycleKey == null) {
log.warn("游戏王奖励事件参数不完整, activityType: {}, origin: {}, cycleKey: {}",
activityType, origin, cycleKey);
return;
}
String lockKey = "game_king_reward:" + activityType + ":" + origin + ":" + cycleKey;
distributedLockUtil.executeWithLock(lockKey, () -> {
try {
log.info("开始处理游戏王奖励, activityType: {}, origin: {}, cycleKey: {}, templateId: {}, rankingUsers: {}",
activityType, origin, cycleKey, templateId,
event.getRankingUsers() != null ? event.getRankingUsers().size() : 0);
if (CollectionUtils.isEmpty(event.getRankingUsers())) {
log.warn("排名用户列表为空,跳过处理");
return null;
}
// 获取活动配置
AppExtCommand cmd = new AppExtCommand();
cmd.setReqSysOrigin(ReqSysOrigin.of("LIKEI"));
ActivityConfigVO activityConfig = weekKingQueenService.getEffectiveActivity(cmd, templateId);
if (activityConfig == null || activityConfig.getConfig() == null) {
log.warn("活动配置不存在, templateId: {}", templateId);
return null;
}
// 转换排名数据
List<RankingRewardGrantCmdExe.RankingUserData> rankingUsers = event.getRankingUsers().stream()
.map(user -> {
RankingRewardGrantCmdExe.RankingUserData data = new RankingRewardGrantCmdExe.RankingUserData();
data.setUserId(user.getUserId());
data.setRank(user.getRank());
data.setScore(user.getScore());
return data;
})
.collect(Collectors.toList());
// 执行奖励发放逻辑
rankingRewardGrantCmdExe.execute(activityType, origin, cycleKey, templateId,
rankingUsers, activityConfig);
log.info("游戏王奖励处理完成, activityType: {}, origin: {}, cycleKey: {}",
activityType, origin, cycleKey);
} catch (Exception e) {
log.error("处理游戏王奖励失败, activityType: {}, origin: {}, cycleKey: {}",
activityType, origin, cycleKey, e);
throw e;
}
return null;
});
}
}

View File

@ -1,23 +1,36 @@
package com.red.circle.other.app.service.activity;
import com.alibaba.fastjson.JSON;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.mq.business.model.event.GameKingRewardEvent;
import com.red.circle.other.app.command.activity.query.HistoricalTop1QryExe;
import com.red.circle.other.app.command.activity.query.RankingDataAccumulateCmdExe;
import com.red.circle.other.app.command.activity.query.RankingListQryExe;
import com.red.circle.other.app.command.activity.query.UserRankQryExe;
import com.red.circle.other.app.dto.clientobject.activity.HistoricalTop1ListCO;
import com.red.circle.other.app.dto.clientobject.activity.RankingListCO;
import com.red.circle.other.app.dto.clientobject.activity.UserRankInfoCO;
import com.red.circle.other.app.dto.clientobject.activity.*;
import com.red.circle.other.app.dto.cmd.UserRankQueryCmd;
import com.red.circle.other.app.dto.cmd.activity.HistoricalTop1QueryCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingListQueryCmd;
import com.red.circle.mq.rocket.business.producer.UserMqMessageService;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.domain.ranking.RankingCycleType;
import com.red.circle.tool.core.collection.CollectionUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 排行榜活动服务实现
*
* 排行榜活动服务实现.
*/
@Slf4j
@Service
@ -28,6 +41,12 @@ public class RankingActivityServiceImpl implements RankingActivityService {
private final RankingListQryExe rankingListQryExe;
private final UserRankQryExe userRankQryExe;
private final HistoricalTop1QryExe historicalTop1QryExe;
private final RedisService redisService;
private final UserMqMessageService userMqMessageService;
private final UserProfileGateway userProfileGateway;
private static final String RANKING_TOP4_CACHE_KEY_PREFIX = "ranking_top4:";
private static final int CACHE_EXPIRE_MINUTES = 5;
@Override
public Boolean accumulateRankingData(RankingDataUpdateCmd cmd) {
@ -48,4 +67,87 @@ public class RankingActivityServiceImpl implements RankingActivityService {
public HistoricalTop1ListCO getHistoricalTop1List(HistoricalTop1QueryCmd cmd) {
return historicalTop1QryExe.execute(cmd);
}
}
@Override
public List<RankingUserVO> getTopFourRankingWithReward(RankingListQueryCmd cmd) {
String cacheKey = buildCacheKey(cmd);
String cachedJson = redisService.getString(cacheKey);
if (cachedJson != null) {
return JSON.parseArray(cachedJson, RankingUserVO.class);
}
RankingListQueryCmd top4Cmd = new RankingListQueryCmd();
top4Cmd.setActivityType(cmd.getActivityType());
top4Cmd.setCycleKey(cmd.getCycleKey());
top4Cmd.setCycleType(RankingCycleType.WEEKLY.getCode());
top4Cmd.setReqUserId(cmd.getReqUserId());
top4Cmd.setReqSysOrigin(cmd.getReqSysOrigin());
top4Cmd.setTopN(10);
RankingListCO result = rankingListQryExe.execute(top4Cmd);
List<RankingUserVO> rankingUsers = null;
if (result != null && CollectionUtils.isNotEmpty(result.getRankingList())) {
Set<Long> userIds = result.getRankingList().stream()
.map(RankingActivityRecordCO::getUserId)
.collect(Collectors.toSet());
Map<Long, UserProfile> userProfileMap = userProfileGateway.mapByUserIds(userIds);
rankingUsers = result.getRankingList().stream()
.limit(4)
.map(user -> {
UserProfile profile = userProfileMap.get(user.getUserId());
RankingUserVO vo = new RankingUserVO();
vo.setUserId(user.getUserId());
vo.setRank(user.getRank());
vo.setQuantity(String.valueOf(user.getQuantity()));
if (profile != null) {
vo.setAccount(profile.getAccount());
vo.setNickname(profile.getUserNickname());
vo.setAvatar(profile.getUserAvatar());
}
return vo;
})
.collect(Collectors.toList());
redisService.setString(cacheKey, JSON.toJSONString(rankingUsers), CACHE_EXPIRE_MINUTES, TimeUnit.MINUTES);
sendGameKingRewardEvent(cmd, result);
}
return rankingUsers;
}
/**
* 发送游戏王奖励消息.
*/
private void sendGameKingRewardEvent(RankingListQueryCmd cmd, RankingListCO result) {
if (CollectionUtils.isEmpty(result.getRankingList())) {
return;
}
GameKingRewardEvent event = new GameKingRewardEvent()
.setActivityType(RankingActivityType.getByCode(cmd.getActivityType()).name())
.setOrigin(cmd.getReqSysOrigin().getOrigin())
.setCycleKey(cmd.getCycleKey())
.setTemplateId(cmd.getTemplateId())
.setRankingUsers(result.getRankingList().stream()
.map(user -> new GameKingRewardEvent.RankingUser()
.setUserId(user.getUserId())
.setRank(user.getRank()))
.collect(Collectors.toList()));
userMqMessageService.sendGameKingRewardEvent(event);
}
/**
* 构建缓存Key.
*/
private String buildCacheKey(RankingListQueryCmd cmd) {
return RANKING_TOP4_CACHE_KEY_PREFIX + RankingActivityType.getByCode(cmd.getActivityType()).name() + ":" + cmd.getCycleKey();
}
}

View File

@ -0,0 +1,51 @@
package com.red.circle.other.app.dto.clientobject.activity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
/**
* 排名用户VO.
*/
@Data
@Accessors(chain = true)
public class RankingUserVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户ID.
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
/**
* 账号.
*/
private String account;
/**
* 昵称.
*/
private String nickname;
/**
* 头像.
*/
private String avatar;
/**
* 排名.
*/
private Integer rank;
/**
* 数量/分数.
*/
private String quantity;
}

View File

@ -57,4 +57,9 @@ public class RankingListQueryCmd extends AppExtCommand {
* 是否包含当前用户排名
*/
private Boolean includeCurrentUser = true;
}
/**
* 模板ID
*/
private String templateId;
}

View File

@ -2,12 +2,15 @@ package com.red.circle.other.app.service.activity;
import com.red.circle.other.app.dto.clientobject.activity.HistoricalTop1ListCO;
import com.red.circle.other.app.dto.clientobject.activity.RankingListCO;
import com.red.circle.other.app.dto.clientobject.activity.RankingUserVO;
import com.red.circle.other.app.dto.clientobject.activity.UserRankInfoCO;
import com.red.circle.other.app.dto.cmd.UserRankQueryCmd;
import com.red.circle.other.app.dto.cmd.activity.HistoricalTop1QueryCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd;
import com.red.circle.other.app.dto.cmd.activity.RankingListQueryCmd;
import java.util.List;
/**
* 排行榜活动服务
*/
@ -44,4 +47,12 @@ public interface RankingActivityService {
* @return 历史Top1列表最近10周
*/
HistoricalTop1ListCO getHistoricalTop1List(HistoricalTop1QueryCmd cmd);
}
/**
* 获取排行榜前四名带缓存和奖励发送
*
* @param cmd 查询命令
* @return 排行榜前四名
*/
List<RankingUserVO> getTopFourRankingWithReward(RankingListQueryCmd cmd);
}

View File

@ -44,6 +44,8 @@ public enum RankingActivityType {
GREEDY_GAME(1068, "GreedyLion", "游戏中奖排行"),
LUCK_GIFT(9999, "LuckGift", "统计送幸运礼物排行"),
KING_GAMES(10, "游戏王活动", "统计游戏数据排行"),
;
/**

View File

@ -0,0 +1,80 @@
package com.red.circle.other.infra.database.mongo.entity.ranking;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 排名奖励快照.
*/
@Data
@Accessors(chain = true)
@Document(collection = "ranking_reward_snapshot")
@CompoundIndexes({
@CompoundIndex(name = "idx_activity_cycle", def = "{'activityType': 1, 'cycleKey': 1, 'origin': 1}"),
@CompoundIndex(name = "idx_snapshot_time", def = "{'snapshotTime': -1}")
})
public class RankingRewardSnapshot implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Id
private String id;
private String activityType;
private String origin;
private String cycleKey;
private String templateId;
private LocalDateTime snapshotTime;
private List<RankingUserSnapshot> rankingUsers;
@Data
@Accessors(chain = true)
public static class RankingUserSnapshot implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long userId;
private Integer rank;
private String rankRange;
private Long score;
private List<RewardItem> rewards;
}
@Data
@Accessors(chain = true)
public static class RewardItem implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String type;
private String detailType;
private String content;
private Integer quantity;
private String badgeName;
}
}

View File

@ -0,0 +1,53 @@
package com.red.circle.other.infra.database.mongo.service.ranking;
import com.red.circle.other.infra.database.mongo.entity.ranking.RankingRewardSnapshot;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
/**
* 排名奖励快照服务.
*/
@Service
@RequiredArgsConstructor
public class RankingRewardSnapshotService {
private final MongoTemplate mongoTemplate;
/**
* 保存快照.
*/
public void save(RankingRewardSnapshot snapshot) {
mongoTemplate.save(snapshot);
}
/**
* 获取最新快照.
*/
public RankingRewardSnapshot getLatestSnapshot(String activityType, String origin, String cycleKey) {
Query query = new Query();
query.addCriteria(Criteria.where("activityType").is(activityType)
.and("origin").is(origin)
.and("cycleKey").is(cycleKey));
query.with(org.springframework.data.domain.Sort.by(
org.springframework.data.domain.Sort.Direction.DESC, "snapshotTime"));
query.limit(1);
return mongoTemplate.findOne(query, RankingRewardSnapshot.class);
}
/**
* 删除旧快照.
*/
public void deleteOldSnapshots(String activityType, String origin, String cycleKey) {
Query query = new Query();
query.addCriteria(Criteria.where("activityType").is(activityType)
.and("origin").is(origin)
.and("cycleKey").is(cycleKey));
mongoTemplate.remove(query, RankingRewardSnapshot.class);
}
}