diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/enums/violation/GameOriginEnum.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/enums/violation/GameOriginEnum.java index c747fae5..5a21d191 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/enums/violation/GameOriginEnum.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/enums/violation/GameOriginEnum.java @@ -6,6 +6,8 @@ public enum GameOriginEnum { HOTGAME, + HKYS, + LINGXIAN, YOMI diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HkysServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HkysServiceImpl.java index 961f67b0..9069fb71 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HkysServiceImpl.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HkysServiceImpl.java @@ -14,170 +14,133 @@ import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.framework.core.exception.ResponseException; import com.red.circle.framework.core.security.UserCredential; import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; +import com.red.circle.other.app.enums.violation.GameOriginEnum; +import com.red.circle.other.app.service.game.common.GameEventService; import com.red.circle.other.domain.gateway.user.UserProfileGateway; import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService; +import com.red.circle.other.infra.database.rds.entity.sys.GameListConfig; +import com.red.circle.other.infra.database.rds.service.sys.GameListConfigService; import com.red.circle.other.inner.enums.config.EnumConfigKey; import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.tool.core.date.LocalDateTimeUtils; import com.red.circle.tool.core.json.JacksonUtils; -import com.red.circle.tool.core.num.ArithmeticUtils; import com.red.circle.tool.core.parse.DataTypeUtils; import com.red.circle.tool.core.text.StringUtils; import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; import com.red.circle.wallet.inner.error.WalletErrorCode; -import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; -import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd.GoldReceiptCmdBuilder; import com.red.circle.wallet.inner.model.dto.WalletReceiptResDTO; import com.red.circle.wallet.inner.model.enums.GoldOrigin; -import java.util.Objects; -import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + /** - * Hkys 游戏服务 实现. + * Hkys 游戏服务实现 * - * @author pengliang on 2022/9/20 + * @author tf */ @Slf4j @Service @RequiredArgsConstructor public class HkysServiceImpl implements GameHkysService { - private final AuthClient authClient; - private final RedisService redisService; - private final WalletGoldClient walletGoldClient; - private final UserProfileGateway userProfileGateway; - private final EnumConfigCacheService enumConfigCacheService; - private final UserProfileAppConvertor userProfileAppConvertor; + private final AuthClient authClient; + private final RedisService redisService; + private final WalletGoldClient walletGoldClient; + private final UserProfileGateway userProfileGateway; + private final EnumConfigCacheService enumConfigCacheService; + private final UserProfileAppConvertor userProfileAppConvertor; + private final GameListConfigService gameListConfigService; + private final GameActivityService gameActivityService; + private final GameEventService gameEventService; - @Override - public HkysResponse getUserProfile(HkysUserProfileQuery query) { - - if (!UserCredential.checkToken(query.getToken(), query.longUid())) { - return new HkysResponse() - .setErrorCode(HyksErrorEnum.AUTHENTICATION_FAILED.getErrorCode()); + @Override + public HkysResponse getUserProfile(HkysUserProfileQuery query) { + if (!UserCredential.checkToken(query.getToken(), query.longUid())) { + return new HkysResponse() + .setErrorCode(HyksErrorEnum.AUTHENTICATION_FAILED.getErrorCode()); + } + UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO( + userProfileGateway.getByUserId(query.longUid())); + if (Objects.isNull(userProfile)) { + return new HkysResponse() + .setErrorCode(HyksErrorEnum.USER_NOT_FOUND.getErrorCode()); + } + if (StringUtils.isBlank(userProfile.getUserAvatar())) { + userProfile.setUserAvatar(enumConfigCacheService.getValue( + EnumConfigKey.DEFAULT_USER_AVATAR, userProfile.getOriginSys())); + } + return new HkysResponse() + .setErrorCode(HyksErrorEnum.SUCCESS.getErrorCode()) + .setData(new HkysUserProfileResponse() + .setUid(Objects.toString(userProfile.getId())) + .setNickname(userProfile.getUserNickname()) + .setAvatar(userProfile.getUserAvatar()) + .setCoin(ResponseAssert.requiredSuccess(walletGoldClient.getBalance(userProfile.getId())) + .getDollarAmount().longValue()) + ); } - UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO( - userProfileGateway.getByUserId(query.longUid()) - ); - if (Objects.isNull(userProfile)) { - return new HkysResponse() - .setErrorCode(HyksErrorEnum.USER_NOT_FOUND.getErrorCode()); - } + @Override + public HkysResponse updateUserCoin(HkysUserCoinUpdate param) { + long startTime = LocalDateTimeUtils.nowEpochMilli(); + Long userId = param.longUid(); - if (StringUtils.isBlank(userProfile.getUserAvatar())) { - userProfile.setUserAvatar( - enumConfigCacheService.getValue(EnumConfigKey.DEFAULT_USER_AVATAR, - userProfile.getOriginSys())); - } + if (!UserCredential.checkToken(param.getToken(), userId)) { + return new HkysResponse() + .setErrorCode(HyksErrorEnum.AUTHENTICATION_FAILED.getErrorCode()); + } - return new HkysResponse() - .setErrorCode(HyksErrorEnum.SUCCESS.getErrorCode()) - .setData(new HkysUserProfileResponse() - .setUid(Objects.toString(userProfile.getId())) - .setNickname(userProfile.getUserNickname()) - .setAvatar(userProfile.getUserAvatar()) - .setCoin( - ResponseAssert.requiredSuccess(walletGoldClient.getBalance(userProfile.getId())) - .getDollarAmount().longValue()) - ); - } + // 幂等检查 + if (!redisService.setIfAbsent("GHYKS:" + param.getOrderId(), 5, TimeUnit.MINUTES)) { + log.warn("重复触发消息: {}", JacksonUtils.toJson(param)); + return new HkysResponse() + .setErrorCode(406); + } + try { + long walletStartTime = LocalDateTimeUtils.nowEpochMilli(); + WalletReceiptResDTO res = walletGoldClient.changeBalance( + gameEventService.buildReceipt(param.getToken(), userId, param.getType(), + GoldOrigin.HKYS_GAME, param.getGameId(), param.getOrderId(), param.getCoin()) + ).getBody(); + long walletCost = LocalDateTimeUtils.nowEpochMilli() - walletStartTime; + long totalCost = LocalDateTimeUtils.nowEpochMilli() - startTime; + if (totalCost >= 500 || walletCost >= 500) { + log.error("(hkys) totalTime={}, walletTime={}, type={}", totalCost, walletCost, param.getType()); + } - @Override - public HkysResponse updateUserCoin(HkysUserCoinUpdate param) { - long startTime = LocalDateTimeUtils.nowEpochMilli(); - Long userId = param.longUid(); - if (!UserCredential.checkToken(param.getToken(), userId)) { - return new HkysResponse() - .setErrorCode(HyksErrorEnum.AUTHENTICATION_FAILED.getErrorCode()); - } + String sysOrigin = UserCredential.parseToken(param.getToken()).getSysOrigin(); + GameListConfig gameConfig = gameListConfigService.getByGameId( + param.getGameId(), GameOriginEnum.HKYS.name(), sysOrigin); - if (!redisService.setIfAbsent("GHYKS:" + param.getOrderId(), 5, - TimeUnit.MINUTES)) { - log.warn("重复触发消息: {}", JacksonUtils.toJson(param)); - return new HkysResponse() - .setErrorCode(406); - } + if (param.getType() == 2 && param.getCoin() > 0) { + gameEventService.onWinEvent(userId, param.getCoin(), GameOriginEnum.HKYS.name(), gameConfig); + gameActivityService.handleGameSpendTask(userId, param.getCoin()); + gameActivityService.incKingGameDaily(userId, param.getCoin(), param.getGameId()); + } else { + gameEventService.onBetEvent(userId, param.getCoin()); + } - long walletStartTime = LocalDateTimeUtils.nowEpochMilli(); + return new HkysResponse() + .setErrorCode(HyksErrorEnum.SUCCESS.getErrorCode()) + .setData(new UpdateUserCoinResponse() + .setEventId(res.getAssetRecordId().toString()) + .setBalance(res.getBalance().getDollarAmount().stripTrailingZeros()) + ); + } catch (ResponseException ex) { + if (Objects.equals(ex.getErrorCode(), WalletErrorCode.INSUFFICIENT_BALANCE.getCode())) { + return new HkysResponse() + .setErrorCode(4004); + } + } catch (Exception ex) { + log.error("ex:{}", Throwables.getStackTraceAsString(ex)); + } - try { - - WalletReceiptResDTO res = walletGoldClient.changeBalance(createReceipt(param)).getBody(); - - long walletEndTime = LocalDateTimeUtils.nowEpochMilli() - walletStartTime; - - long totalTime = LocalDateTimeUtils.nowEpochMilli() - startTime; - - if (totalTime >= 500 || walletEndTime >= 500) { - log.error("totalTime={},walletEndTim={},type={}" - , totalTime - , walletEndTime - , param.getType()); - } - - - return new HkysResponse() - .setErrorCode(HyksErrorEnum.SUCCESS.getErrorCode()) - .setData(new UpdateUserCoinResponse() - .setEventId(res.getAssetRecordId().toString()) - .setBalance(res.getBalance().getDollarAmount().stripTrailingZeros()) - ); - } catch (ResponseException ex) { - if (Objects.equals(ex.getErrorCode(), WalletErrorCode.INSUFFICIENT_BALANCE.getCode())) { return new HkysResponse() - .setErrorCode(4004); - } - } catch (Exception ex) { - log.error("ex:{}", Throwables.getStackTraceAsString(ex)); + .setErrorCode(HyksErrorEnum.SERVER_ERROR.getErrorCode()); } - - return new HkysResponse() - .setErrorCode(HyksErrorEnum.SERVER_ERROR.getErrorCode()); - } - - private GoldReceiptCmd createReceipt(HkysUserCoinUpdate param) { - UserCredential userCredential = UserCredential.parseToken(param.getToken()); - return createGoldReceiptCmdBuilder(param.getType()) - .userId(param.longUid()) - .sysOrigin(userCredential.getSysOrigin()) - .origin(GoldOrigin.HKYS_GAME.name() + "_" + param.getGameId()) - .originDescribe(GoldOrigin.HKYS_GAME.getDesc() + "[" + param.getGameId() + "]") - .eventId(param.getOrderId()) - .amount(ArithmeticUtils.toBigDecimal(param.getCoin())) - .build(); - } - - private GoldReceiptCmdBuilder createGoldReceiptCmdBuilder(Integer type) { - if (Objects.equals(type, 1)) { - return GoldReceiptCmd.builder().appExpenditure(); - } - - if (Objects.equals(type, 2)) { - return GoldReceiptCmd.builder().appIncome(); - } - - throw new IllegalArgumentException("3rd party param [type] error: " + type); - } - - /** - * 强制验证令牌. - * - * @return true 通过,false 不通过 - */ - private boolean checkTokenStrict(String token, Long userId) { - - UserCredential tokenContent = UserCredential.parseToken(token); - - UserCredential registerCredential = ResponseAssert.requiredSuccess( - authClient.getUserCredential(DataTypeUtils.toLong(userId))); - - return UserCredential.checkToken(tokenContent, registerCredential) - && Objects.equals(tokenContent.getUserId(), userId); - } - } diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HotGameServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HotGameServiceImpl.java index d84044e6..18d75148 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HotGameServiceImpl.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/HotGameServiceImpl.java @@ -8,56 +8,48 @@ import com.red.circle.component.game.hotgame.response.HotGameCoin; import com.red.circle.component.game.hotgame.response.HotGameErrorEnum; import com.red.circle.component.game.hotgame.response.HotGameResponse; import com.red.circle.component.redis.service.RedisService; -import com.red.circle.external.inner.endpoint.message.ImGroupClient; -import com.red.circle.external.inner.model.cmd.message.BroadcastGroupMsgBodyCmd; -import com.red.circle.external.inner.model.enums.message.GroupMessageTypeEnum; import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.framework.core.dto.ReqSysOrigin; import com.red.circle.framework.core.exception.ResponseException; import com.red.circle.framework.core.security.UserCredential; -import com.red.circle.mq.rocket.business.producer.TaskMqMessage; import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd; -import com.red.circle.other.app.dto.cmd.task.RoomDailyTaskProgressUpdateCmd; import com.red.circle.other.app.enums.violation.GameOriginEnum; import com.red.circle.other.app.service.activity.ActivityRechargeTicketService; import com.red.circle.other.app.service.activity.RankingActivityService; +import com.red.circle.other.app.service.game.common.GameEventService; import com.red.circle.other.app.service.game.override.domain.HotGameUserProfile; import com.red.circle.other.app.service.game.override.service.HotGameService; import com.red.circle.other.app.service.task.RoomDailyTaskProgressService; -import com.red.circle.other.app.util.DateTimeAsiaRiyadhUtils; -import com.red.circle.other.app.util.OfficialNoticeUtils; -import com.red.circle.other.domain.game.GameRankingIncrementCmd; -import com.red.circle.other.domain.gateway.game.GameRankingGateway; import com.red.circle.other.domain.gateway.user.UserProfileGateway; import com.red.circle.other.domain.ranking.RankingActivityType; import com.red.circle.other.domain.ranking.RankingCycleType; import com.red.circle.other.domain.ranking.RankingDimension; import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService; import com.red.circle.other.infra.database.cache.service.other.GameListCacheService; +import com.red.circle.other.infra.database.rds.dto.game.WeeklyVipInfo; import com.red.circle.other.infra.database.rds.entity.sys.GameListConfig; import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService; -import com.red.circle.other.infra.database.rds.dto.game.WeeklyVipInfo; import com.red.circle.other.infra.database.rds.service.game.GameVipLevelWeeklyService; import com.red.circle.other.infra.database.rds.service.sys.GameListConfigService; import com.red.circle.other.inner.enums.config.EnumConfigKey; import com.red.circle.other.inner.enums.game.VipLevelRule; -import com.red.circle.other.inner.enums.task.RoomDailyTaskCode; import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.tool.core.date.LocalDateTimeUtils; import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils; import com.red.circle.tool.core.date.ZonedId; import com.red.circle.tool.core.json.JacksonUtils; -import com.red.circle.tool.core.num.ArithmeticUtils; import com.red.circle.tool.core.parse.DataTypeUtils; import com.red.circle.tool.core.sequence.IdWorkerUtils; import com.red.circle.tool.core.text.StringUtils; import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; import com.red.circle.wallet.inner.error.WalletErrorCode; -import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; -import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd.GoldReceiptCmdBuilder; import com.red.circle.wallet.inner.model.dto.WalletReceiptResDTO; import com.red.circle.wallet.inner.model.enums.GoldOrigin; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; @@ -66,14 +58,15 @@ import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.TimeUnit; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - +/** + * HotGame 游戏服务实现 + * + * @author tf + */ @Slf4j @Service @RequiredArgsConstructor @@ -84,10 +77,7 @@ public class HotGameServiceImpl implements HotGameService { private final UserProfileGateway userProfileGateway; private final EnumConfigCacheService enumConfigCacheService; private final UserProfileAppConvertor userProfileAppConvertor; - private final TaskMqMessage taskMqMessage; - private final ImGroupClient imGroupClient; private final GameListConfigService gameListConfigService; - private final GameRankingGateway gameRankingGateway; private final GameListCacheService gameListCacheService; private final RankingActivityService rankingActivityService; private final GameActivityService gameActivityService; @@ -95,9 +85,10 @@ public class HotGameServiceImpl implements HotGameService { private final GameVipLevelWeeklyService gameVipLevelWeeklyService; private final ActivityRechargeTicketService activityRechargeTicketService; private final RoomDailyTaskProgressService roomDailyTaskProgressService; - + private final GameEventService gameEventService; + @Value("${red-circle.game-rank.gameid}") - private String gameId; + private String rankGameId; @Value("${red-circle.game-rank.endTime}") private String endTime; @@ -106,24 +97,20 @@ public class HotGameServiceImpl implements HotGameService { @Override public HotGameResponse getUserProfile(HotGameUserProfileQuery query) { - if (!UserCredential.checkToken(query.getToken(), query.longUid())) { return new HotGameResponse() .setErrorCode(HotGameErrorEnum.AUTHENTICATION_FAILED.getErrorCode()); } - UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO( userProfileGateway.getByUserId(query.longUid())); if (Objects.isNull(userProfile)) { return new HotGameResponse() .setErrorCode(HotGameErrorEnum.USER_NOT_FOUND.getErrorCode()); } - if (StringUtils.isBlank(userProfile.getUserAvatar())) { - userProfile.setUserAvatar(enumConfigCacheService.getValue(EnumConfigKey.DEFAULT_USER_AVATAR, - userProfile.getOriginSys())); + userProfile.setUserAvatar(enumConfigCacheService.getValue( + EnumConfigKey.DEFAULT_USER_AVATAR, userProfile.getOriginSys())); } - Integer vipLevel = getVipLevel(query.longUid(), userProfile.getCountryCode()); return new HotGameResponse() .setErrorCode(HotGameErrorEnum.SUCCESS.getErrorCode()) @@ -132,214 +119,57 @@ public class HotGameServiceImpl implements HotGameService { .uid(Objects.toString(userProfile.getId())) .nickname(userProfile.getUserNickname()) .avatar(userProfile.getUserAvatar()) - .coin( - ResponseAssert.requiredSuccess(walletGoldClient.getBalance(userProfile.getId())) - .getDollarAmount().longValue()).build() + .coin(ResponseAssert.requiredSuccess(walletGoldClient.getBalance(userProfile.getId())) + .getDollarAmount().longValue()) + .build() ); } - public Integer getVipLevel(Long userId, String countryCode) { - LocalDateTime now = LocalDateTime.now(); - // 巴基斯坦返回0 其他正常 - if ("PK".equals(countryCode)) { - return 0; - } - - // 【新增】检查是否为每月的 1-7号 或 16-22号 -// int dayOfMonth = now.getDayOfMonth(); -// if ((dayOfMonth >= 1 && dayOfMonth <= 7) || (dayOfMonth >= 16 && dayOfMonth <= 22)) { -// return 0; -// } - - try { - String cacheKey = VIP_LEVEL_CACHE_KEY + userId; - Object cachedLevel = redisService.redisTemplate().opsForValue().get(cacheKey); - if (cachedLevel != null) { - return Integer.parseInt(cachedLevel.toString()); - } - - LocalDate firstRechargeDate = userActivityRechargeService.getFirstRechargeDate(userId, VIP_ACTIVITY_ID); - if (firstRechargeDate == null) { - redisService.redisTemplate().opsForValue().set(cacheKey, 0, 1, TimeUnit.DAYS); - return 0; - } - - int[] weekDurations = VipLevelRule.calculateWeekDurations(userId, firstRechargeDate); - int week1Days = weekDurations[0]; - int week2Days = weekDurations[1]; - int week3Days = weekDurations[2]; - - LocalDate today = ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate(); - long daysSinceFirst = ChronoUnit.DAYS.between(firstRechargeDate, today); - long completedCycles = daysSinceFirst / 21; - long dayInCycle = daysSinceFirst % 21; - - int normalizedWeek; - int weekInCycle; - LocalDate weekStartDate; - LocalDate weekEndDate; - - if (dayInCycle < week1Days) { - normalizedWeek = 1; - weekInCycle = 1; - weekStartDate = firstRechargeDate.plusDays(completedCycles * 21); - weekEndDate = weekStartDate.plusDays(week1Days - 1); - } else if (dayInCycle < week1Days + week2Days) { - normalizedWeek = 2; - weekInCycle = 2; - weekStartDate = firstRechargeDate.plusDays(completedCycles * 21 + week1Days); - weekEndDate = weekStartDate.plusDays(week2Days - 1); - } else { - normalizedWeek = 3; - weekInCycle = 3; - weekStartDate = firstRechargeDate.plusDays(completedCycles * 21 + week1Days + week2Days); - weekEndDate = weekStartDate.plusDays(week3Days - 1); - } - - LocalDate cycleStartDate = firstRechargeDate.plusDays(completedCycles * 21); - LocalDate cycleEndDate = cycleStartDate.plusDays(20); - - WeeklyVipInfo lastWeekInfo = gameVipLevelWeeklyService.getLastWeekInfo(userId, VIP_ACTIVITY_ID, weekStartDate); - - BigDecimal lastWeekInherited = calculateLastWeekInherited(lastWeekInfo, weekInCycle, completedCycles); - - BigDecimal thisWeekRecharge = userActivityRechargeService.getRechargeAmountByDateRange( - userId, VIP_ACTIVITY_ID, weekStartDate, weekEndDate - ); - - int displayLevel = VipLevelRule.calculateVipLevelWithInheritance( - normalizedWeek, thisWeekRecharge, lastWeekInherited - ); - // 应用保底逻辑 - int startLevel = calculateStartLevel(lastWeekInfo, weekInCycle, completedCycles); - displayLevel = Math.max(displayLevel, startLevel); - - BigDecimal thisWeekInherited = thisWeekRecharge.add(lastWeekInherited); - - gameVipLevelWeeklyService.saveOrUpdateWeeklyLevel( - userId, VIP_ACTIVITY_ID, (int) (completedCycles * 3 + normalizedWeek), - weekStartDate, weekEndDate, displayLevel, thisWeekRecharge, - normalizedWeek, thisWeekInherited - ); - - redisService.redisTemplate().opsForValue().set(cacheKey, displayLevel, 1, TimeUnit.DAYS); - - return displayLevel; - - } catch (Exception e) { - log.error("获取VIP等级异常, userId:{}, error:{}", userId, e.getMessage(), e); - return 0; - } - - } - - private BigDecimal calculateLastWeekInherited(WeeklyVipInfo lastWeekInfo, int weekInCycle, long completedCycles) { - BigDecimal lastWeekInherited = Optional.ofNullable(lastWeekInfo) - .map(WeeklyVipInfo::getInheritedAmount) - .orElse(BigDecimal.ZERO); - - // 第二个周期及以后,第一周需要除3再除2 - if (completedCycles > 0 && weekInCycle == 1) { - return lastWeekInherited - .divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP) - .divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); - } - - if (completedCycles > 0 && weekInCycle == 2) { - BigDecimal lastCycleInherited = lastWeekInherited.subtract(Optional.ofNullable(lastWeekInfo).map(WeeklyVipInfo::getRechargeAmount).orElse(BigDecimal.ZERO)).max(BigDecimal.ZERO); - return lastWeekInherited.subtract(lastCycleInherited).divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); - } - - // 原有逻辑:第二周除2,第三周除2再除2 - if (weekInCycle == 2) { - return lastWeekInherited.divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); - } else if (weekInCycle == 3) { - return lastWeekInherited.divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP) - .divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); - } - - return lastWeekInherited; - } - - private Integer calculateStartLevel(WeeklyVipInfo lastWeekInfo, int weekInCycle, long completedCycles) { - // 只在新周期的第一周判断 - if (completedCycles > 0 && weekInCycle == 1 && lastWeekInfo != null) { - // 检查上周期是否达到过V3 - if (lastWeekInfo.getFinalLevel() != null && lastWeekInfo.getFinalLevel() >= 3) { - return 2; // 保底V2 - } - } - return 0; // 默认无保底 - } - - @Override public HotGameResponse updateUserCoin(HotGameUserCoinUpdate param) { long startTime = LocalDateTimeUtils.nowEpochMilli(); Long userId = param.longUid(); + if (!UserCredential.checkToken(param.getToken(), userId)) { return new HotGameResponse() - .setData(new HotGameCoin() - .setBalanceCoin(0L) - .setResponseId(IdWorkerUtils.getIdStr()) - ) + .setData(new HotGameCoin().setBalanceCoin(0L).setResponseId(IdWorkerUtils.getIdStr())) .setErrorCode(HotGameErrorEnum.AUTHENTICATION_FAILED.getErrorCode()); } - if (!redisService.setIfAbsent("GHOT:" + param.getOrderId(), 5, - TimeUnit.MINUTES)) { + // 幂等检查 + String idempotentKey = "GHOT:" + param.getOrderId(); + if (!redisService.setIfAbsent(idempotentKey, 5, TimeUnit.MINUTES)) { log.warn("重复触发消息: {}", JacksonUtils.toJson(param)); return new HotGameResponse() - .setData(new HotGameCoin() - .setBalanceCoin(0L) - .setResponseId("0") - ).setErrorCode(406); + .setData(new HotGameCoin().setBalanceCoin(0L).setResponseId("0")) + .setErrorCode(406); } + try { long walletStartTime = LocalDateTimeUtils.nowEpochMilli(); - - WalletReceiptResDTO res = walletGoldClient.changeBalance(createReceipt(param)).getBody(); - - long walletEndTime = LocalDateTimeUtils.nowEpochMilli() - walletStartTime; - - long totalTime = LocalDateTimeUtils.nowEpochMilli() - startTime; - - if (totalTime >= 500 || walletEndTime >= 500) { - log.error("(hotgame) totalTime={},walletEndTim={},type={}" - , totalTime - , walletEndTime - , param.getType()); + WalletReceiptResDTO res = walletGoldClient.changeBalance( + gameEventService.buildReceipt(param.getToken(), userId, param.getType(), + GoldOrigin.HOT_GAME, param.getGameId(), param.getOrderId(), param.getCoin()) + ).getBody(); + long walletCost = LocalDateTimeUtils.nowEpochMilli() - walletStartTime; + long totalCost = LocalDateTimeUtils.nowEpochMilli() - startTime; + if (totalCost >= 500 || walletCost >= 500) { + log.error("(hotgame) totalTime={}, walletTime={}, type={}", totalCost, walletCost, param.getType()); } String sysOrigin = UserCredential.parseToken(param.getToken()).getSysOrigin(); - GameListConfig gameListConfig = gameListCacheService.getByGameIdWithCache( - param.getGameId(), - GameOriginEnum.HOTGAME.name(), - sysOrigin, + GameListConfig gameConfig = gameListCacheService.getByGameIdWithCache( + param.getGameId(), GameOriginEnum.HOTGAME.name(), sysOrigin, () -> gameListConfigService.getByGameId(param.getGameId(), GameOriginEnum.HOTGAME.name(), sysOrigin) ); - // type = 1 支出 type = 2 收入 - sendBroadcast(param, gameListConfig); - //累计游戏排行榜 - incGameRankingRecord(param, gameListConfig); - //游戏参与打榜活动 - incGameRankingActivity(param); - - //FruitParty - incGameRankingTmpActivity(param); - - //处理游戏获得任务 - if (param.getType() == 2) { + if (param.getType() == 2 && param.getCoin() > 0) { + gameEventService.onWinEvent(userId, param.getCoin(), GameOriginEnum.HOTGAME.name(), gameConfig); gameActivityService.handleGameSpendTask(userId, param.getCoin()); - - // 累加用户获得的金币并发送抽奖券 -// activityRechargeTicketService.accumulatedRechargeAndAddTicket(userId, Math.abs(param.getCoin()), MonthlyRechargeType.UNDEFINED); - } - - // 处理游戏消费人任务 - else { - updateRoomDailyTask(param.getCoin(), userId); + gameActivityService.incKingGameDaily(userId, param.getCoin(), param.getGameId()); + incFruitPartyRankingActivity(param); + } else { + gameEventService.onBetEvent(userId, param.getCoin()); } return new HotGameResponse() @@ -362,133 +192,137 @@ public class HotGameServiceImpl implements HotGameService { .setErrorCode(HotGameErrorEnum.SERVER_ERROR.getErrorCode()); } - private void updateRoomDailyTask(Long coins, Long userId) { - RoomDailyTaskCode taskType = RoomDailyTaskCode.PERSONAL_GAME_CONSUME; - String redisKey = "room:daily:task:progress:" + taskType.name() + ":" + userId + ":" + ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate(); - Long total = redisService.increment(redisKey, Math.abs(coins.intValue())); - redisService.expire(redisKey, DateTimeAsiaRiyadhUtils.getSecondsUntilMidnight(), TimeUnit.SECONDS); - roomDailyTaskProgressService.updateTaskProgress( - new RoomDailyTaskProgressUpdateCmd() - .setUserId(userId) - .setTaskCode(taskType.name()) - .setProgressValue(total.intValue()) + // ------------------------------------------------------------------ // + // HotGame 特有:FruitParty 临时排行榜 + // ------------------------------------------------------------------ // + + private void incFruitPartyRankingActivity(HotGameUserCoinUpdate param) { + if (ZonedDateTimeAsiaRiyadhUtils.now().isAfter(getFruitPartyFinishTime())) { + return; + } + if (rankGameId == null || !rankGameId.equals(param.getGameId())) { + return; + } + RankingDataUpdateCmd cmd = new RankingDataUpdateCmd(); + cmd.setUserId(DataTypeUtils.toLong(param.getUid())); + cmd.setUserSex(1); + cmd.setActivityType(RankingActivityType.FRUIT_PARTY.getCode()); + cmd.setCycleType(RankingCycleType.WEEKLY.getCode()); + cmd.setCycleKey("20260129"); + cmd.setDimension(RankingDimension.GAME_WIN.getCode()); + cmd.setIncrementQuantity(param.getCoin()); + cmd.setBizNo(param.getGameId()); + cmd.setReqSysOrigin(ReqSysOrigin.of(SysOriginPlatformEnum.ATYOU.name())); + rankingActivityService.accumulateRankingData(cmd); + } + + private ZonedDateTime getFruitPartyFinishTime() { + return ZonedDateTime.of( + LocalDateTime.parse(endTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME), + ZonedId.ASIA_RIYADH.getZonedId() ); } - private void incGameRankingTmpActivity(HotGameUserCoinUpdate request) { - if (ZonedDateTimeAsiaRiyadhUtils.now().isAfter(getFinishTime())) { - return; - } + // ------------------------------------------------------------------ // + // HotGame 特有:VIP 等级 + // ------------------------------------------------------------------ // - if (gameId != null && gameId.equals(request.getGameId()) && request.getType() == 2 && request.getCoin() > 0) { - RankingDataUpdateCmd receiverCmd = new RankingDataUpdateCmd(); - receiverCmd.setUserId(DataTypeUtils.toLong(request.getUid())); - receiverCmd.setUserSex(1); - receiverCmd.setActivityType(RankingActivityType.FRUIT_PARTY.getCode()); - receiverCmd.setCycleType(RankingCycleType.WEEKLY.getCode()); - receiverCmd.setCycleKey("20260129"); - receiverCmd.setDimension(RankingDimension.GAME_WIN.getCode()); - receiverCmd.setIncrementQuantity(request.getCoin()); - receiverCmd.setBizNo(request.getGameId()); - receiverCmd.setReqSysOrigin(ReqSysOrigin.of(SysOriginPlatformEnum.ATYOU.name())); - rankingActivityService.accumulateRankingData(receiverCmd); + public Integer getVipLevel(Long userId, String countryCode) { + if ("PK".equals(countryCode)) { + return 0; + } + try { + String cacheKey = VIP_LEVEL_CACHE_KEY + userId; + Object cachedLevel = redisService.redisTemplate().opsForValue().get(cacheKey); + if (cachedLevel != null) { + return Integer.parseInt(cachedLevel.toString()); + } + LocalDate firstRechargeDate = userActivityRechargeService.getFirstRechargeDate(userId, VIP_ACTIVITY_ID); + if (firstRechargeDate == null) { + redisService.redisTemplate().opsForValue().set(cacheKey, 0, 1, TimeUnit.DAYS); + return 0; + } + int[] weekDurations = VipLevelRule.calculateWeekDurations(userId, firstRechargeDate); + int week1Days = weekDurations[0]; + int week2Days = weekDurations[1]; + int week3Days = weekDurations[2]; + LocalDate today = ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate(); + long daysSinceFirst = ChronoUnit.DAYS.between(firstRechargeDate, today); + long completedCycles = daysSinceFirst / 21; + long dayInCycle = daysSinceFirst % 21; + int normalizedWeek; + int weekInCycle; + LocalDate weekStartDate; + LocalDate weekEndDate; + if (dayInCycle < week1Days) { + normalizedWeek = 1; + weekInCycle = 1; + weekStartDate = firstRechargeDate.plusDays(completedCycles * 21); + weekEndDate = weekStartDate.plusDays(week1Days - 1); + } else if (dayInCycle < week1Days + week2Days) { + normalizedWeek = 2; + weekInCycle = 2; + weekStartDate = firstRechargeDate.plusDays(completedCycles * 21 + week1Days); + weekEndDate = weekStartDate.plusDays(week2Days - 1); + } else { + normalizedWeek = 3; + weekInCycle = 3; + weekStartDate = firstRechargeDate.plusDays(completedCycles * 21 + week1Days + week2Days); + weekEndDate = weekStartDate.plusDays(week3Days - 1); + } + WeeklyVipInfo lastWeekInfo = gameVipLevelWeeklyService.getLastWeekInfo(userId, VIP_ACTIVITY_ID, weekStartDate); + BigDecimal lastWeekInherited = calculateLastWeekInherited(lastWeekInfo, weekInCycle, completedCycles); + BigDecimal thisWeekRecharge = userActivityRechargeService.getRechargeAmountByDateRange( + userId, VIP_ACTIVITY_ID, weekStartDate, weekEndDate); + int displayLevel = VipLevelRule.calculateVipLevelWithInheritance( + normalizedWeek, thisWeekRecharge, lastWeekInherited); + int startLevel = calculateStartLevel(lastWeekInfo, weekInCycle, completedCycles); + displayLevel = Math.max(displayLevel, startLevel); + BigDecimal thisWeekInherited = thisWeekRecharge.add(lastWeekInherited); + gameVipLevelWeeklyService.saveOrUpdateWeeklyLevel( + userId, VIP_ACTIVITY_ID, (int) (completedCycles * 3 + normalizedWeek), + weekStartDate, weekEndDate, displayLevel, thisWeekRecharge, + normalizedWeek, thisWeekInherited); + redisService.redisTemplate().opsForValue().set(cacheKey, displayLevel, 1, TimeUnit.DAYS); + return displayLevel; + } catch (Exception e) { + log.error("获取VIP等级异常, userId:{}, error:{}", userId, e.getMessage(), e); + return 0; } } - private ZonedDateTime getFinishTime() { - return ZonedDateTime.of(LocalDateTime.parse(endTime, - DateTimeFormatter.ISO_LOCAL_DATE_TIME), - ZonedId.ASIA_RIYADH.getZonedId()); + private BigDecimal calculateLastWeekInherited(WeeklyVipInfo lastWeekInfo, int weekInCycle, long completedCycles) { + BigDecimal lastWeekInherited = Optional.ofNullable(lastWeekInfo) + .map(WeeklyVipInfo::getInheritedAmount) + .orElse(BigDecimal.ZERO); + if (completedCycles > 0 && weekInCycle == 1) { + return lastWeekInherited + .divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP) + .divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); + } + if (completedCycles > 0 && weekInCycle == 2) { + BigDecimal lastCycleInherited = lastWeekInherited + .subtract(Optional.ofNullable(lastWeekInfo).map(WeeklyVipInfo::getRechargeAmount).orElse(BigDecimal.ZERO)) + .max(BigDecimal.ZERO); + return lastWeekInherited.subtract(lastCycleInherited) + .divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); + } + if (weekInCycle == 2) { + return lastWeekInherited.divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); + } else if (weekInCycle == 3) { + return lastWeekInherited + .divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP) + .divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); + } + return lastWeekInherited; } - private void incGameRankingRecord(HotGameUserCoinUpdate request, GameListConfig gameListConfig) { - if (request.getType() != 2 || gameListConfig == null || request.getCoin() <= 0) { - return; + private Integer calculateStartLevel(WeeklyVipInfo lastWeekInfo, int weekInCycle, long completedCycles) { + if (completedCycles > 0 && weekInCycle == 1 && lastWeekInfo != null) { + if (lastWeekInfo.getFinalLevel() != null && lastWeekInfo.getFinalLevel() >= 3) { + return 2; + } } - - GameRankingIncrementCmd build = GameRankingIncrementCmd.builder() - .gameOrigin(GameOriginEnum.HOTGAME.name()) - .gameId(gameListConfig.getGameId()) - .gameName(gameListConfig.getName()) - .gameCover(gameListConfig.getCover()) - .gameUrl(gameListConfig.getGameCode()) - .sysOrigin(gameListConfig.getSysOrigin()) - .amount(request.getCoin()) - .periodType("ALL") - .build(); - gameRankingGateway.incrementPrizeAmount(build); + return 0; } - - private void incGameRankingActivity(HotGameUserCoinUpdate param) { - if (param.getType() != null && param.getType() == 2 && param.getCoin() > 0) { - RankingDataUpdateCmd receiverCmd = new RankingDataUpdateCmd(); - receiverCmd.setUserId(DataTypeUtils.toLong(param.getUid())); - receiverCmd.setUserSex(1); - receiverCmd.setActivityType(RankingActivityType.KING_GAMES.getCode()); - receiverCmd.setCycleType(RankingCycleType.WEEKLY.getCode()); - receiverCmd.setCycleKey(ZonedDateTimeAsiaRiyadhUtils.nowWeekMondayToInt().toString()); - receiverCmd.setDimension(RankingDimension.GAME_WIN.getCode()); - receiverCmd.setIncrementQuantity(param.getCoin()); - receiverCmd.setBizNo(param.getGameId()); - receiverCmd.setReqSysOrigin(ReqSysOrigin.of(SysOriginPlatformEnum.ATYOU.name())); - rankingActivityService.accumulateRankingData(receiverCmd); - - //游戏王日榜 - gameActivityService.incKingGameDaily(DataTypeUtils.toLong(param.getUid()), param.getCoin(), param.getGameId()); - } - } - - private void sendBroadcast(HotGameUserCoinUpdate request, GameListConfig gameListConfig) { - if (gameListConfig == null) { - return; - } - - BigDecimal cost = enumConfigCacheService - .getValueBigDecimal(EnumConfigKey.GAME_BROADCAST_AMOUNT, gameListConfig.getSysOrigin()) ; - if (cost == null || cost.compareTo(BigDecimal.ZERO) <= 0) { - cost = BigDecimal.valueOf(5000L); - } - - if (request.getType() != null && request.getType() == 2 && request.getCoin() >= cost.longValue()) { - UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO( - userProfileGateway.getByUserId(DataTypeUtils.toLong(request.getUid()))); - - Map build = OfficialNoticeUtils.buildUserProfile(userProfile); - build.put("gameUrl", gameListConfig.getCover()); - build.put("currencyDiff", request.getCoin()); - - imGroupClient.sendMessageBroadcast( - BroadcastGroupMsgBodyCmd.builder() - .toPlatform(SysOriginPlatformEnum.ATYOU) - .type(GroupMessageTypeEnum.GAME_BAISHUN_WIN) - .data(build) - .build() - ); - } - } - - private GoldReceiptCmdBuilder createGoldReceiptCmdBuilder(Integer type) { - if (Objects.equals(type, 1)) { - return GoldReceiptCmd.builder().appExpenditure(); - } - - if (Objects.equals(type, 2)) { - return GoldReceiptCmd.builder().appIncome(); - } - - throw new IllegalArgumentException("3rd party param [type] error: " + type); - } - - private GoldReceiptCmd createReceipt(HotGameUserCoinUpdate param) { - UserCredential userCredential = UserCredential.parseToken(param.getToken()); - return createGoldReceiptCmdBuilder(param.getType()) - .userId(param.longUid()) - .sysOrigin(userCredential.getSysOrigin()) - .origin(GoldOrigin.HOT_GAME.name() + "_" + param.getGameId()) - .originDescribe(GoldOrigin.HOT_GAME.getDesc() + "[" + param.getGameId() + "]") - .eventId(param.getOrderId()) - .amount(ArithmeticUtils.toBigDecimal(param.getCoin())) - .build(); - } - } diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/YomiGameServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/YomiGameServiceImpl.java index 5fcb81c8..1f919479 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/YomiGameServiceImpl.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/YomiGameServiceImpl.java @@ -3,25 +3,18 @@ package com.red.circle.other.app.service.game; import com.red.circle.auth.inner.endpoint.AuthClient; import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; import com.red.circle.component.redis.service.RedisService; -import com.red.circle.external.inner.endpoint.message.ImGroupClient; -import com.red.circle.external.inner.model.cmd.message.BroadcastGroupMsgBodyCmd; -import com.red.circle.external.inner.model.enums.message.GroupMessageTypeEnum; import com.red.circle.framework.core.dto.ReqSysOrigin; import com.red.circle.framework.core.security.UserCredential; import com.red.circle.framework.dto.ResultResponse; import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd; import com.red.circle.other.app.dto.cmd.party3rd.*; -import com.red.circle.other.app.dto.cmd.task.RoomDailyTaskProgressUpdateCmd; import com.red.circle.other.app.enums.violation.GameOriginEnum; import com.red.circle.other.app.service.activity.ActivityRechargeTicketService; import com.red.circle.other.app.service.activity.RankingActivityService; +import com.red.circle.other.app.service.game.common.GameEventService; import com.red.circle.other.app.service.game.override.service.YomiGameService; import com.red.circle.other.app.service.task.RoomDailyTaskProgressService; -import com.red.circle.other.app.util.DateTimeAsiaRiyadhUtils; -import com.red.circle.other.app.util.OfficialNoticeUtils; -import com.red.circle.other.domain.game.GameRankingIncrementCmd; -import com.red.circle.other.domain.gateway.game.GameRankingGateway; 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; @@ -31,14 +24,8 @@ import com.red.circle.other.infra.config.YomiConfig; import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService; import com.red.circle.other.infra.database.rds.entity.sys.GameListConfig; import com.red.circle.other.infra.database.rds.service.sys.GameListConfigService; -import com.red.circle.other.inner.enums.config.EnumConfigKey; -import com.red.circle.other.inner.enums.task.RoomDailyTaskCode; -import com.red.circle.other.inner.model.dto.user.UserProfileDTO; -import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils; -import com.red.circle.tool.core.num.ArithmeticUtils; 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.dto.WalletReceiptResDTO; import com.red.circle.wallet.inner.model.enums.GoldOrigin; import lombok.RequiredArgsConstructor; @@ -46,12 +33,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import java.math.BigDecimal; -import java.util.Optional; import java.util.concurrent.TimeUnit; /** * Yomi游戏服务实现 + * + * @author tf */ @Slf4j @Service @@ -67,15 +54,11 @@ public class YomiGameServiceImpl implements YomiGameService { private final UserProfileGateway userProfileGateway; private final WalletGoldClient walletGoldClient; private final GameListConfigService gameListConfigService; - private final EnumConfigCacheService enumConfigCacheService; - private final ImGroupClient imGroupClient; - private final UserProfileAppConvertor userProfileAppConvertor; private final RankingActivityService rankingActivityService; private final GameActivityService gameActivityService; - private final GameRankingGateway gameRankingGateway; private final ActivityRechargeTicketService activityRechargeTicketService; private final RoomDailyTaskProgressService roomDailyTaskProgressService; - private final RedisService redisService; + private final GameEventService gameEventService; @Override public YomiTokenCO getToken(YomiGetTokenCmd cmd) { @@ -94,12 +77,10 @@ public class YomiGameServiceImpl implements YomiGameService { if (balanceResp == null || balanceResp.getBody() == null) { throw new RuntimeException("查询用户余额失败"); } - String token = credResp.getBody().token(); long expireAt = System.currentTimeMillis() / 1000 + yomiConfig.getTokenExpireSeconds(); redisTemplate.opsForValue().set(buildTokenCacheKey(token), cmd.getPlatUserId(), yomiConfig.getTokenExpireSeconds(), TimeUnit.SECONDS); - return YomiTokenCO.builder() .platUserId(cmd.getPlatUserId()) .platUserOpenId("") // 项目暂无 openId 概念,按协议传空 @@ -154,10 +135,9 @@ public class YomiGameServiceImpl implements YomiGameService { throw new Exception("token与用户ID不匹配"); } - // 幂等检查:requestId 已处理则直接返回当前余额 + // 幂等检查 String idempotentKey = IDEMPOTENT_KEY_PREFIX + cmd.getRequestId(); - String processed = redisTemplate.opsForValue().get(idempotentKey); - if (processed != null) { + if (gameEventService.isProcessed(idempotentKey)) { log.info("yomi余额变动重复请求,直接返回: requestId={}", cmd.getRequestId()); ResultResponse balanceResp = walletGoldClient.getBalance(userId); if (balanceResp == null || balanceResp.getBody() == null) { @@ -167,29 +147,32 @@ public class YomiGameServiceImpl implements YomiGameService { return new YomiBalanceCO(balanceResp.getBody().getDollarAmount().longValue()); } - WalletReceiptResDTO res = walletGoldClient.changeBalance(createReceipt(cmd)).getBody(); + WalletReceiptResDTO res = walletGoldClient.changeBalance( + gameEventService.buildReceipt(cmd.getPlatToken(), userId, cmd.getReasonType(), + GoldOrigin.YOMI_GAME, gameIdStr, + String.valueOf(cmd.getGameRoundId()), cmd.getGold()) + ).getBody(); if (res == null || res.getBalance() == null) { throw new Exception("wallet changeBalance failed, response is null"); } long currentBalance = res.getBalance().getDollarAmount().longValue(); - // 执行成功后写入幂等标记 - redisTemplate.opsForValue().set(idempotentKey, "1", IDEMPOTENT_EXPIRE_SECONDS, TimeUnit.SECONDS); + gameEventService.markProcessed(idempotentKey, IDEMPOTENT_EXPIRE_SECONDS); String sysOrigin = UserCredential.parseToken(cmd.getPlatToken()).getSysOrigin(); - GameListConfig gameConfig = gameListConfigService.getByGameId(gameIdStr, GameOriginEnum.YOMI.name(), sysOrigin); + GameListConfig gameConfig = gameListConfigService.getByGameId( + gameIdStr, GameOriginEnum.YOMI.name(), sysOrigin); if (gameConfig == null) { log.error("游戏配置不存在, gameUid={}", cmd.getGameUid()); return new YomiBalanceCO(currentBalance); } if (!isBet(cmd)) { - sendBroadcast(userId, cmd.getGold().intValue(), gameConfig.getCover()); - incGameRankingRecord(cmd.getGold().intValue(), gameConfig); - incGameRankingActivity(userId, cmd.getGold().intValue(), gameIdStr); + gameEventService.onWinEvent(userId, cmd.getGold(), GameOriginEnum.YOMI.name(), gameConfig); gameActivityService.handleGameSpendTask(userId, cmd.getGold()); + gameActivityService.incKingGameDaily(userId, cmd.getGold(), gameIdStr); } else { - updateRoomDailyTask(cmd.getGold(), userId); + gameEventService.onBetEvent(userId, cmd.getGold()); } return new YomiBalanceCO(currentBalance); @@ -200,10 +183,6 @@ public class YomiGameServiceImpl implements YomiGameService { } } - // ------------------------------------------------------------------ // - // 私有方法 - // ------------------------------------------------------------------ // - private static boolean isBet(YomiChangeBalanceCmd cmd) { return Integer.valueOf(1).equals(cmd.getReasonType()); } @@ -211,91 +190,4 @@ public class YomiGameServiceImpl implements YomiGameService { private String buildTokenCacheKey(String token) { return "yomi:token:" + token; } - - private GoldReceiptCmd createReceipt(YomiChangeBalanceCmd cmd) { - UserCredential userCredential = UserCredential.parseToken(cmd.getPlatToken()); - String gameUidStr = String.valueOf(cmd.getGameUid()); - return createGoldReceiptCmdBuilder(cmd.getReasonType()) - .userId(Long.parseLong(cmd.getPlatUserId())) - .sysOrigin(userCredential.getSysOrigin()) - .origin(GoldOrigin.YOMI_GAME.name() + "_" + gameUidStr) - .originDescribe(GoldOrigin.YOMI_GAME.getDesc() + "[" + gameUidStr + "]") - .eventId(String.valueOf(cmd.getGameRoundId())) - .amount(ArithmeticUtils.toBigDecimal(Math.abs(cmd.getGold()))) - .build(); - } - - private GoldReceiptCmd.GoldReceiptCmdBuilder createGoldReceiptCmdBuilder(Integer reasonType) { - if (Integer.valueOf(1).equals(reasonType)) { - return GoldReceiptCmd.builder().appExpenditure(); - } - if (Integer.valueOf(2).equals(reasonType)) { - return GoldReceiptCmd.builder().appIncome(); - } - throw new IllegalArgumentException("yomi game param [reasonType] error: " + reasonType); - } - - private void updateRoomDailyTask(Long coins, Long userId) { - RoomDailyTaskCode taskType = RoomDailyTaskCode.PERSONAL_GAME_CONSUME; - String redisKey = "room:daily:task:progress:" + taskType.name() + ":" + userId + ":" + ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate(); - Long total = redisService.increment(redisKey, Math.abs(coins.intValue())); - redisService.expire(redisKey, DateTimeAsiaRiyadhUtils.getSecondsUntilMidnight(), TimeUnit.SECONDS); - roomDailyTaskProgressService.updateTaskProgress( - new RoomDailyTaskProgressUpdateCmd() - .setUserId(userId) - .setTaskCode(taskType.name()) - .setProgressValue(total.intValue()) - ); - } - - private void sendBroadcast(Long userId, Integer rewardAmount, String gameUrl) { - if (rewardAmount == null || rewardAmount <= 0) { - return; - } - BigDecimal cost = enumConfigCacheService.getValueBigDecimal(EnumConfigKey.GAME_BROADCAST_AMOUNT, SysOriginPlatformEnum.ATYOU.name()); - int thresholdAmount = Optional.ofNullable(cost).orElse(new BigDecimal("5000")).intValue(); - if (rewardAmount < thresholdAmount) { - return; - } - UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO(userProfileGateway.getByUserId(userId)); - java.util.Map build = OfficialNoticeUtils.buildUserProfile(userProfile); - build.put("gameUrl", gameUrl); - build.put("currencyDiff", rewardAmount); - imGroupClient.sendMessageBroadcast( - BroadcastGroupMsgBodyCmd.builder() - .toPlatform(SysOriginPlatformEnum.ATYOU) - .type(GroupMessageTypeEnum.GAME_BAISHUN_WIN) - .data(build) - .build() - ); - } - - private void incGameRankingRecord(Integer rewardAmount, GameListConfig gameListConfig) { - GameRankingIncrementCmd build = GameRankingIncrementCmd.builder() - .gameOrigin(GameOriginEnum.YOMI.name()) - .gameId(gameListConfig.getGameId()) - .gameName(gameListConfig.getName()) - .gameCover(gameListConfig.getCover()) - .gameUrl(gameListConfig.getGameCode()) - .sysOrigin(gameListConfig.getSysOrigin()) - .amount(rewardAmount.longValue()) - .periodType("ALL") - .build(); - gameRankingGateway.incrementPrizeAmount(build); - } - - private void incGameRankingActivity(Long userId, Integer rewardAmount, String gameId) { - RankingDataUpdateCmd receiverCmd = new RankingDataUpdateCmd(); - receiverCmd.setUserId(userId); - receiverCmd.setUserSex(1); - receiverCmd.setActivityType(RankingActivityType.KING_GAMES.getCode()); - receiverCmd.setCycleType(RankingCycleType.WEEKLY.getCode()); - receiverCmd.setCycleKey(ZonedDateTimeAsiaRiyadhUtils.nowWeekMondayToInt().toString()); - receiverCmd.setDimension(RankingDimension.GAME_WIN.getCode()); - receiverCmd.setIncrementQuantity(rewardAmount.longValue()); - receiverCmd.setBizNo(gameId); - receiverCmd.setReqSysOrigin(ReqSysOrigin.of(SysOriginPlatformEnum.ATYOU.name())); - rankingActivityService.accumulateRankingData(receiverCmd); - gameActivityService.incKingGameDaily(userId, rewardAmount.longValue(), gameId); - } } diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/common/GameEventService.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/common/GameEventService.java new file mode 100644 index 00000000..f4ccbc5a --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/game/common/GameEventService.java @@ -0,0 +1,208 @@ +package com.red.circle.other.app.service.game.common; + +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.component.redis.service.RedisService; +import com.red.circle.external.inner.endpoint.message.ImGroupClient; +import com.red.circle.external.inner.model.cmd.message.BroadcastGroupMsgBodyCmd; +import com.red.circle.external.inner.model.enums.message.GroupMessageTypeEnum; +import com.red.circle.framework.core.dto.ReqSysOrigin; +import com.red.circle.framework.core.security.UserCredential; +import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; +import com.red.circle.other.app.dto.cmd.activity.RankingDataUpdateCmd; +import com.red.circle.other.app.dto.cmd.task.RoomDailyTaskProgressUpdateCmd; +import com.red.circle.other.app.service.activity.RankingActivityService; +import com.red.circle.other.app.service.task.RoomDailyTaskProgressService; +import com.red.circle.other.app.util.DateTimeAsiaRiyadhUtils; +import com.red.circle.other.app.util.OfficialNoticeUtils; +import com.red.circle.other.domain.game.GameRankingIncrementCmd; +import com.red.circle.other.domain.gateway.game.GameRankingGateway; +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.ranking.RankingActivityType; +import com.red.circle.other.domain.ranking.RankingCycleType; +import com.red.circle.other.domain.ranking.RankingDimension; +import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService; +import com.red.circle.other.infra.database.rds.entity.sys.GameListConfig; +import com.red.circle.other.inner.enums.config.EnumConfigKey; +import com.red.circle.other.inner.enums.task.RoomDailyTaskCode; +import com.red.circle.other.inner.model.dto.user.UserProfileDTO; +import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils; +import com.red.circle.tool.core.num.ArithmeticUtils; +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.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * 第三方游戏公共事件处理 + * + * @author tf + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class GameEventService { + + private final RedisTemplate redisTemplate; + private final RedisService redisService; + private final WalletGoldClient walletGoldClient; + private final UserProfileGateway userProfileGateway; + private final UserProfileAppConvertor userProfileAppConvertor; + private final EnumConfigCacheService enumConfigCacheService; + private final ImGroupClient imGroupClient; + private final GameRankingGateway gameRankingGateway; + private final RankingActivityService rankingActivityService; + private final RoomDailyTaskProgressService roomDailyTaskProgressService; + + // ------------------------------------------------------------------ // + // 幂等 + // ------------------------------------------------------------------ // + + /** + * 幂等检查,true 表示已处理过 + */ + public boolean isProcessed(String idempotentKey) { + return redisTemplate.opsForValue().get(idempotentKey) != null; + } + + /** + * 标记已处理 + */ + public void markProcessed(String idempotentKey, long ttlSeconds) { + redisTemplate.opsForValue().set(idempotentKey, "1", ttlSeconds, TimeUnit.SECONDS); + } + + // ------------------------------------------------------------------ // + // 钱包收支命令构建 + // ------------------------------------------------------------------ // + + /** + * 构建钱包收支命令 + * + * @param type 1=支出(投注) 2=收入(返奖) + * @param origin 游戏来源枚举 + * @param gameId 游戏ID字符串 + * @param eventId 幂等事件ID(orderId / roundId) + * @param amount 金额绝对值 + */ + public GoldReceiptCmd buildReceipt(String token, Long userId, Integer type, + GoldOrigin origin, String gameId, + String eventId, long amount) { + UserCredential credential = UserCredential.parseToken(token); + return buildReceiptCmdBuilder(type) + .userId(userId) + .sysOrigin(credential.getSysOrigin()) + .origin(origin.name() + "_" + gameId) + .originDescribe(origin.getDesc() + "[" + gameId + "]") + .eventId(eventId) + .amount(ArithmeticUtils.toBigDecimal(Math.abs(amount))) + .build(); + } + + private GoldReceiptCmd.GoldReceiptCmdBuilder buildReceiptCmdBuilder(Integer type) { + if (Integer.valueOf(1).equals(type)) { + return GoldReceiptCmd.builder().appExpenditure(); + } + if (Integer.valueOf(2).equals(type)) { + return GoldReceiptCmd.builder().appIncome(); + } + throw new IllegalArgumentException("3rd game param [type] error: " + type); + } + + // ------------------------------------------------------------------ // + // 返奖事件(type=2) + // ------------------------------------------------------------------ // + + /** + * 返奖后的业务增强:广播 + 游戏排行榜记录 + 打榜活动 + 游戏王日榜 + 获得任务 + */ + public void onWinEvent(Long userId, Long gold, String gameOrigin, GameListConfig gameConfig) { + if (gold == null || gold <= 0 || gameConfig == null) { + return; + } + sendBroadcast(userId, gold, gameConfig); + incGameRankingRecord(gold, gameOrigin, gameConfig); + incGameRankingActivity(userId, gold, gameConfig.getGameId(), gameConfig.getSysOrigin()); + } + + private void sendBroadcast(Long userId, Long gold, GameListConfig gameConfig) { + BigDecimal cost = enumConfigCacheService.getValueBigDecimal( + EnumConfigKey.GAME_BROADCAST_AMOUNT, gameConfig.getSysOrigin()); + long threshold = Optional.ofNullable(cost) + .filter(c -> c.compareTo(BigDecimal.ZERO) > 0) + .orElse(BigDecimal.valueOf(5000L)) + .longValue(); + if (gold < threshold) { + return; + } + UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO( + userProfileGateway.getByUserId(userId)); + Map data = OfficialNoticeUtils.buildUserProfile(userProfile); + data.put("gameUrl", gameConfig.getCover()); + data.put("currencyDiff", gold); + imGroupClient.sendMessageBroadcast( + BroadcastGroupMsgBodyCmd.builder() + .toPlatform(SysOriginPlatformEnum.ATYOU) + .type(GroupMessageTypeEnum.GAME_BAISHUN_WIN) + .data(data) + .build() + ); + } + + private void incGameRankingRecord(Long gold, String gameOrigin, GameListConfig gameConfig) { + gameRankingGateway.incrementPrizeAmount( + GameRankingIncrementCmd.builder() + .gameOrigin(gameOrigin) + .gameId(gameConfig.getGameId()) + .gameName(gameConfig.getName()) + .gameCover(gameConfig.getCover()) + .gameUrl(gameConfig.getGameCode()) + .sysOrigin(gameConfig.getSysOrigin()) + .amount(gold) + .periodType("ALL") + .build() + ); + } + + private void incGameRankingActivity(Long userId, Long gold, String gameId, String sysOrigin) { + RankingDataUpdateCmd cmd = new RankingDataUpdateCmd(); + cmd.setUserId(userId); + cmd.setUserSex(1); + cmd.setActivityType(RankingActivityType.KING_GAMES.getCode()); + cmd.setCycleType(RankingCycleType.WEEKLY.getCode()); + cmd.setCycleKey(ZonedDateTimeAsiaRiyadhUtils.nowWeekMondayToInt().toString()); + cmd.setDimension(RankingDimension.GAME_WIN.getCode()); + cmd.setIncrementQuantity(gold); + cmd.setBizNo(gameId); + cmd.setReqSysOrigin(ReqSysOrigin.of(sysOrigin)); + rankingActivityService.accumulateRankingData(cmd); + } + + // ------------------------------------------------------------------ // + // 投注事件(type=1) + // ------------------------------------------------------------------ // + + /** + * 投注后的业务增强:房间日任务 + */ + public void onBetEvent(Long userId, Long gold) { + RoomDailyTaskCode taskType = RoomDailyTaskCode.PERSONAL_GAME_CONSUME; + String redisKey = "room:daily:task:progress:" + taskType.name() + ":" + userId + + ":" + ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate(); + Long total = redisService.increment(redisKey, Math.abs(gold.intValue())); + redisService.expire(redisKey, DateTimeAsiaRiyadhUtils.getSecondsUntilMidnight(), TimeUnit.SECONDS); + roomDailyTaskProgressService.updateTaskProgress( + new RoomDailyTaskProgressUpdateCmd() + .setUserId(userId) + .setTaskCode(taskType.name()) + .setProgressValue(total.intValue()) + ); + } +}