From 1ab0d93bd582640d6ae3cd893d68dc4b0af5b74f Mon Sep 17 00:00:00 2001 From: tianfeng <769204422@qq.com> Date: Wed, 14 Jan 2026 16:30:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=91=E9=80=81cp=E6=A6=9C?= =?UTF-8?q?=E5=8D=95=E5=A5=96=E5=8A=B1=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/enums/SendPropsOrigin.java | 5 + .../wallet/inner/model/enums/GoldOrigin.java | 5 + .../award/CpRankingRewardManager.java | 290 ++++++++++++++++++ .../app/scheduler/CpRankingRewardTask.java | 119 +++++++ .../java/activity/CpRankingRewardTest.java | 59 ++++ 5 files changed, 478 insertions(+) create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/manager/activity/award/CpRankingRewardManager.java create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/CpRankingRewardTask.java create mode 100644 rc-service/rc-service-other/other-start/src/test/java/activity/CpRankingRewardTest.java diff --git a/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java b/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java index d5dc70f3..bb47cc5c 100644 --- a/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java +++ b/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java @@ -276,6 +276,11 @@ public enum SendPropsOrigin { LUCK_DOLLAR_REWARD("Luck Dollar Rewards"), + /** + * 每赛季CP相互赠送礼物榜单. + */ + SEASON_CP_GIFT(""), + ; private final String desc; diff --git a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java index 5962bc6e..f1ba2d1a 100644 --- a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java +++ b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java @@ -742,6 +742,11 @@ public enum GoldOrigin { * CP告白信封 */ CP_LOVE_LETTER("CP love letter"), + + /** + * cp榜单(周榜和季度榜) + */ + CP_RANKING_REWARD("CP ranking reward"), ; private final String desc; diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/manager/activity/award/CpRankingRewardManager.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/manager/activity/award/CpRankingRewardManager.java new file mode 100644 index 00000000..1c5f4d58 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/manager/activity/award/CpRankingRewardManager.java @@ -0,0 +1,290 @@ +package com.red.circle.other.app.manager.activity.award; + +import com.google.common.collect.Lists; +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.common.business.enums.SendPropsOrigin; +import com.red.circle.other.infra.common.activity.PropsActivitySendCommon; +import com.red.circle.other.infra.common.activity.send.SendRewardGroup; +import com.red.circle.other.infra.database.mongo.entity.user.count.WeekCpValueCount; +import com.red.circle.other.infra.database.mongo.service.user.count.WeekCpValueCountService; +import com.red.circle.other.infra.database.rds.entity.activity.PropsActivityRuleConfig; +import com.red.circle.other.infra.database.rds.service.activity.PropsActivityRuleConfigService; +import com.red.circle.other.infra.utils.SeasonUtils; +import com.red.circle.other.inner.enums.activity.PropsActivityTypeEnum; +import com.red.circle.tool.core.collection.CollectionUtils; +import com.red.circle.tool.core.date.DateFormatConstant; +import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils; +import com.red.circle.tool.core.date.ZonedDateTimeUtils; +//import com.red.circle.wallet.client.gold.WalletGoldClient; +//import com.red.circle.wallet.inner.cmd.GoldReceiptCmd; +//import com.red.circle.wallet.inner.enums.GoldOrigin; +//import com.red.circle.wallet.inner.model.PennyAmount; +import com.red.circle.tool.core.tuple.PennyAmount; +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.apache.commons.lang3.tuple.ImmutablePair; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * CP排行榜奖励发送管理器. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CpRankingRewardManager { + + private final WeekCpValueCountService weekCpValueCountService; + private final PropsActivityRuleConfigService propsActivityRuleConfigService; + private final PropsActivitySendCommon sendPropsManager; + private final WalletGoldClient walletGoldClient; + private final StringRedisTemplate stringRedisTemplate; + + /** + * 发送上周周榜前三名奖励 + */ + @Transactional(rollbackFor = Exception.class) + public void sendLastWeekTopReward(SysOriginPlatformEnum sysOrigin) { + String lastWeekDate = getLastWeekDate(); + log.info("[CP周榜] 开始发送上周周榜前三奖励, 系统平台: {}, 上周日期: {}", + sysOrigin.name(), lastWeekDate); + + try { + // 1. 获取上周前三名 + List lastWeekTop3 = weekCpValueCountService + .listLastWeekTop(sysOrigin.name(), 3); + + if (CollectionUtils.isEmpty(lastWeekTop3)) { + log.warn("[CP周榜] 上周前三不存在, 系统平台: {}", sysOrigin.name()); + return; + } + + // 2. 获取周榜奖励配置 + List rewardConfigs = propsActivityRuleConfigService + .listPlatformByActivityType(sysOrigin.getSysOrigin(), PropsActivityTypeEnum.WEEK_CP_GIFT); + + if (CollectionUtils.isEmpty(rewardConfigs) || rewardConfigs.size() < 3) { + log.error("[CP周榜] 奖励配置不足3个, 系统平台: {}, 实际数量: {}", + sysOrigin.name(), rewardConfigs == null ? 0 : rewardConfigs.size()); + return; + } + + // 3. 发放奖励 + for (int rank = 0; rank < Math.min(lastWeekTop3.size(), 3); rank++) { + WeekCpValueCount cpCount = lastWeekTop3.get(rank); + PropsActivityRuleConfig rewardConfig = rewardConfigs.get(rank); + + // 发送奖励 + sendWeekCpReward(sysOrigin, cpCount, rewardConfig, rank + 1, lastWeekDate); + } + + log.info("[CP周榜] 上周周榜前三奖励发放完成, 系统平台: {}", sysOrigin.name()); + + } catch (Exception e) { + log.error("[CP周榜] 发送上周周榜奖励异常, 系统平台: {}", sysOrigin.name(), e); + throw e; + } + } + + /** + * 发送上赛季前三名奖励 + */ + @Transactional(rollbackFor = Exception.class) + public void sendLastSeasonTopReward(SysOriginPlatformEnum sysOrigin) { + String lastSeasonCycleKey = SeasonUtils.getLastSeasonCycleKey(); + log.info("[CP赛季榜] 开始发送上赛季前三奖励, 系统平台: {}, 上赛季: {}", + sysOrigin.name(), lastSeasonCycleKey); + + try { + // 1. 获取上赛季前三名 + List lastSeasonTop3 = weekCpValueCountService + .listLastSeasonTop(sysOrigin.name(), 3); + + if (CollectionUtils.isEmpty(lastSeasonTop3)) { + log.warn("[CP赛季榜] 上赛季前三不存在, 系统平台: {}", sysOrigin.name()); + return; + } + + // 2. 获取赛季榜奖励配置 + List rewardConfigs = propsActivityRuleConfigService + .listPlatformByActivityType(sysOrigin.getSysOrigin(), PropsActivityTypeEnum.SEASON_CP_GIFT); + + if (CollectionUtils.isEmpty(rewardConfigs) || rewardConfigs.size() < 3) { + log.error("[CP赛季榜] 奖励配置不足3个, 系统平台: {}, 实际数量: {}", + sysOrigin.name(), rewardConfigs == null ? 0 : rewardConfigs.size()); + return; + } + + // 3. 发放奖励 + for (int rank = 0; rank < Math.min(lastSeasonTop3.size(), 3); rank++) { + WeekCpValueCount cpCount = lastSeasonTop3.get(rank); + PropsActivityRuleConfig rewardConfig = rewardConfigs.get(rank); + + // 发送奖励 + sendSeasonCpReward(sysOrigin, cpCount, rewardConfig, rank + 1, lastSeasonCycleKey); + } + + log.info("[CP赛季榜] 上赛季前三奖励发放完成, 系统平台: {}", sysOrigin.name()); + + } catch (Exception e) { + log.error("[CP赛季榜] 发送上赛季奖励异常, 系统平台: {}", sysOrigin.name(), e); + throw e; + } + } + + /** + * 发送周榜CP奖励 + */ + private void sendWeekCpReward(SysOriginPlatformEnum sysOrigin, + WeekCpValueCount cpCount, + PropsActivityRuleConfig rewardConfig, + int rank, + String weekDate) { + ImmutablePair userPair = weekCpValueCountService.parseUserIdPair(cpCount.getId()); + Long userOne = userPair.getLeft(); + Long userTwo = userPair.getRight(); + + // 计算金币奖励 + double[] percentages = {0.04, 0.03, 0.02}; + double percentage = percentages[rank - 1]; + long goldReward = (long) (cpCount.getQuantity() * percentage); + + // 发送给用户1 + String eventIdOne = String.format("WK_CP_RW_%s_%d_%d", weekDate, userOne, rank); + if (checkAndSetIdempotent(eventIdOne, 15)) { + sendGoldAndProps(sysOrigin, userOne, goldReward, rewardConfig.getResourceGroupId(), + rewardConfig.getId(), SendPropsOrigin.WEEK_CP_GIFT, eventIdOne); + log.info("[CP周榜] 排名 {} 用户 {} 奖励发放成功, 金币: {}, 道具组: {}", + rank, userOne, goldReward, rewardConfig.getResourceGroupId()); + } + + // 发送给用户2 + String eventIdTwo = String.format("WK_CP_RW_%s_%d_%d", weekDate, userTwo, rank); + if (checkAndSetIdempotent(eventIdTwo, 15)) { + sendGoldAndProps(sysOrigin, userTwo, goldReward, rewardConfig.getResourceGroupId(), + rewardConfig.getId(), SendPropsOrigin.WEEK_CP_GIFT, eventIdTwo); + log.info("[CP周榜] 排名 {} 用户 {} 奖励发放成功, 金币: {}, 道具组: {}", + rank, userTwo, goldReward, rewardConfig.getResourceGroupId()); + } + } + + /** + * 发送赛季榜CP奖励 + */ + private void sendSeasonCpReward(SysOriginPlatformEnum sysOrigin, + WeekCpValueCount cpCount, + PropsActivityRuleConfig rewardConfig, + int rank, + String seasonCycleKey) { + ImmutablePair userPair = weekCpValueCountService.parseUserIdPair(cpCount.getId()); + Long userOne = userPair.getLeft(); + Long userTwo = userPair.getRight(); + + // 赛季榜直接发送resourceGroupId奖励,不计算金币 + // 发送给用户1 + String eventIdOne = String.format("SN_CP_RW_%s_%d_%d", seasonCycleKey, userOne, rank); + if (checkAndSetIdempotent(eventIdOne, 60)) { + sendProps(sysOrigin, userOne, rewardConfig.getResourceGroupId(), + rewardConfig.getId(), SendPropsOrigin.SEASON_CP_GIFT); + log.info("[CP赛季榜] 排名 {} 用户 {} 奖励发放成功, 道具组: {}", + rank, userOne, rewardConfig.getResourceGroupId()); + } + + // 发送给用户2 + String eventIdTwo = String.format("SN_CP_RW_%s_%d_%d", seasonCycleKey, userTwo, rank); + if (checkAndSetIdempotent(eventIdTwo, 60)) { + sendProps(sysOrigin, userTwo, rewardConfig.getResourceGroupId(), + rewardConfig.getId(), SendPropsOrigin.SEASON_CP_GIFT); + log.info("[CP赛季榜] 排名 {} 用户 {} 奖励发放成功, 道具组: {}", + rank, userTwo, rewardConfig.getResourceGroupId()); + } + } + + /** + * 发送金币和道具 + */ + private void sendGoldAndProps(SysOriginPlatformEnum sysOrigin, + Long userId, + long goldAmount, + Long resourceGroupId, + Long trackId, + SendPropsOrigin origin, + String eventId) { + // 发送金币 + addUserGold(userId, sysOrigin.name(), goldAmount, eventId); + + // 发送道具 + sendProps(sysOrigin, userId, resourceGroupId, trackId, origin); + } + + /** + * 发送道具 + */ + private void sendProps(SysOriginPlatformEnum sysOrigin, + Long userId, + Long resourceGroupId, + Long trackId, + SendPropsOrigin origin) { + sendPropsManager.sendActivityGroup(SendRewardGroup.builder() + .trackId(trackId) + .sysOrigin(sysOrigin) + .acceptUserId(userId) + .resourceGroupId(resourceGroupId) + .origin(origin) + .build()); + } + + /** + * 增加用户金币 + */ + private void addUserGold(Long userId, String sysOrigin, Long amount, String eventId) { + GoldReceiptCmd build = GoldReceiptCmd.builder() + .appIncome() + .userId(userId) + .amount(PennyAmount.ofDollar(amount)) + .eventId(eventId) + .sysOrigin(sysOrigin) + .origin(GoldOrigin.CP_RANKING_REWARD) + .build(); + + try { + walletGoldClient.changeBalance(build); + } catch (Exception e) { + log.error("增加金币失败 userId={} amount={} eventId={}", userId, amount, eventId, e); + throw new RuntimeException("Failed to increase gold coins"); + } + } + + /** + * 幂等性检查 + */ + private boolean checkAndSetIdempotent(String eventId, int expireDays) { + String key = "CP_REWARD_IDEMPOTENT:" + eventId; + Boolean result = stringRedisTemplate.opsForValue() + .setIfAbsent(key, "1", expireDays, TimeUnit.DAYS); + + if (Boolean.FALSE.equals(result)) { + log.warn("奖励已发放, 跳过重复发送, eventId={}", eventId); + return false; + } + return true; + } + + /** + * 获取上周一日期 + */ + private String getLastWeekDate() { + return ZonedDateTimeUtils.format( + ZonedDateTimeAsiaRiyadhUtils.getLastWeekMonday(), + DateFormatConstant.yyyyMMdd + ); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/CpRankingRewardTask.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/CpRankingRewardTask.java new file mode 100644 index 00000000..4e061612 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/CpRankingRewardTask.java @@ -0,0 +1,119 @@ +package com.red.circle.other.app.scheduler; + +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.component.redis.annotation.TaskCacheLock; +import com.red.circle.other.app.manager.activity.award.CpRankingRewardManager; +import com.red.circle.other.infra.utils.SeasonUtils; +import com.red.circle.tool.core.date.LocalDateTimeUtils; +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.Component; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * CP排行榜奖励发放定时任务. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CpRankingRewardTask { + + private final CpRankingRewardManager cpRankingRewardManager; + private final StringRedisTemplate stringRedisTemplate; + + private static final String LAST_SEASON_KEY = "CP_REWARD:LAST_SEASON_CYCLE"; + + /** + * 每周一 0点10秒执行 - 发送上周周榜前三奖励 + */ + @Scheduled(cron = "10 0 0 ? * MON", zone = "Asia/Riyadh") + @TaskCacheLock(key = "CP_WEEK_RANKING_REWARD_TASK", expireSecond = 86400) + public void sendWeekRankingReward() { + long startTime = System.currentTimeMillis(); + log.warn("========== CP周榜奖励发放定时任务开始 =========="); + + try { + List platforms = List.of(SysOriginPlatformEnum.LIKEI); + + int successCount = 0; + int errorCount = 0; + + for (SysOriginPlatformEnum platform : platforms) { + try { + cpRankingRewardManager.sendLastWeekTopReward(platform); + successCount++; + log.info("[CP周榜] 平台 {} 奖励发放成功", platform.name()); + } catch (Exception e) { + errorCount++; + log.error("[CP周榜] 平台 {} 奖励发放失败", platform.name(), e); + } + } + + log.warn("========== CP周榜奖励发放定时任务完成, 耗时: {} ms, 成功: {}, 失败: {} ==========", + System.currentTimeMillis() - startTime, successCount, errorCount); + + } catch (Exception e) { + log.error("========== CP周榜奖励发放定时任务异常 ==========", e); + } + } + + /** + * 每天 0点0秒执行 - 检查赛季是否切换,如果切换则发送上赛季前三奖励 + */ + @Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Riyadh") + @TaskCacheLock(key = "CP_SEASON_RANKING_REWARD_TASK", expireSecond = 86400) + public void sendSeasonRankingReward() { + long startTime = System.currentTimeMillis(); + log.info("========== CP赛季榜奖励发放检查开始 =========="); + + try { + // 获取当前赛季cycleKey + String currentSeasonCycleKey = SeasonUtils.getCurrentSeasonCycleKey(); + + // 获取上次执行时的赛季cycleKey + String lastSeasonCycleKey = stringRedisTemplate.opsForValue().get(LAST_SEASON_KEY); + + log.info("[CP赛季榜] 当前赛季: {}, 上次记录赛季: {}", currentSeasonCycleKey, lastSeasonCycleKey); + + // 如果赛季切换了,发放上赛季奖励 + if (lastSeasonCycleKey != null && !lastSeasonCycleKey.equals(currentSeasonCycleKey)) { + log.warn("========== 检测到赛季切换,开始发放上赛季奖励 =========="); + log.warn("[CP赛季榜] 赛季切换: {} -> {}", lastSeasonCycleKey, currentSeasonCycleKey); + + List platforms = List.of(SysOriginPlatformEnum.LIKEI); + + int successCount = 0; + int errorCount = 0; + + for (SysOriginPlatformEnum platform : platforms) { + try { + cpRankingRewardManager.sendLastSeasonTopReward(platform); + successCount++; + log.info("[CP赛季榜] 平台 {} 奖励发放成功", platform.name()); + } catch (Exception e) { + errorCount++; + log.error("[CP赛季榜] 平台 {} 奖励发放失败", platform.name(), e); + } + } + + log.warn("========== CP赛季榜奖励发放完成, 耗时: {} ms, 成功: {}, 失败: {} ==========", + System.currentTimeMillis() - startTime, successCount, errorCount); + } else { + log.info("[CP赛季榜] 赛季未切换,无需发放奖励"); + } + + // 更新缓存的赛季cycleKey(无论是否切换都更新,确保下次能正确判断) + stringRedisTemplate.opsForValue().set(LAST_SEASON_KEY, currentSeasonCycleKey, 90, TimeUnit.DAYS); + + log.info("========== CP赛季榜奖励发放检查完成, 总耗时: {} ms ==========", + System.currentTimeMillis() - startTime); + + } catch (Exception e) { + log.error("========== CP赛季榜奖励发放检查异常 ==========", e); + } + } +} diff --git a/rc-service/rc-service-other/other-start/src/test/java/activity/CpRankingRewardTest.java b/rc-service/rc-service-other/other-start/src/test/java/activity/CpRankingRewardTest.java new file mode 100644 index 00000000..9570c6d2 --- /dev/null +++ b/rc-service/rc-service-other/other-start/src/test/java/activity/CpRankingRewardTest.java @@ -0,0 +1,59 @@ +package activity; + +import com.red.circle.OtherServiceApplication; +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.other.app.manager.activity.award.CpRankingRewardManager; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * CP排行榜奖励测试类. + */ +@Slf4j +@SpringBootTest(classes = OtherServiceApplication.class) +@RunWith(SpringRunner.class) +public class CpRankingRewardTest { + + @Autowired + private CpRankingRewardManager cpRankingRewardManager; + + /** + * 测试发送上周周榜前三奖励 + */ + @Test + public void testSendLastWeekTopReward() { + log.info("========== 开始测试发送上周周榜前三奖励 =========="); + + try { + SysOriginPlatformEnum platform = SysOriginPlatformEnum.LIKEI; + + cpRankingRewardManager.sendLastWeekTopReward(platform); + + log.info("========== 上周周榜前三奖励发送测试完成 =========="); + } catch (Exception e) { + log.error("========== 上周周榜前三奖励发送测试异常 ==========", e); + } + } + + /** + * 测试发送上赛季前三奖励 + */ + @Test + public void testSendLastSeasonTopReward() { + log.info("========== 开始测试发送上赛季前三奖励 =========="); + + try { + SysOriginPlatformEnum platform = SysOriginPlatformEnum.LIKEI; + + cpRankingRewardManager.sendLastSeasonTopReward(platform); + + log.info("========== 上赛季前三奖励发送测试完成 =========="); + } catch (Exception e) { + log.error("========== 上赛季前三奖励发送测试异常 ==========", e); + } + } +}