refactor: extract GameEventService for game event common logic, add broadcast/ranking/task to HkysServiceImpl

This commit is contained in:
tianfeng 2026-05-26 19:24:56 +08:00
parent 03a276d022
commit b450ced3cf
5 changed files with 487 additions and 588 deletions

View File

@ -6,6 +6,8 @@ public enum GameOriginEnum {
HOTGAME,
HKYS,
LINGXIAN,
YOMI

View File

@ -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<HkysUserProfileResponse> getUserProfile(HkysUserProfileQuery query) {
if (!UserCredential.checkToken(query.getToken(), query.longUid())) {
return new HkysResponse<HkysUserProfileResponse>()
.setErrorCode(HyksErrorEnum.AUTHENTICATION_FAILED.getErrorCode());
@Override
public HkysResponse<HkysUserProfileResponse> getUserProfile(HkysUserProfileQuery query) {
if (!UserCredential.checkToken(query.getToken(), query.longUid())) {
return new HkysResponse<HkysUserProfileResponse>()
.setErrorCode(HyksErrorEnum.AUTHENTICATION_FAILED.getErrorCode());
}
UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO(
userProfileGateway.getByUserId(query.longUid()));
if (Objects.isNull(userProfile)) {
return new HkysResponse<HkysUserProfileResponse>()
.setErrorCode(HyksErrorEnum.USER_NOT_FOUND.getErrorCode());
}
if (StringUtils.isBlank(userProfile.getUserAvatar())) {
userProfile.setUserAvatar(enumConfigCacheService.getValue(
EnumConfigKey.DEFAULT_USER_AVATAR, userProfile.getOriginSys()));
}
return new HkysResponse<HkysUserProfileResponse>()
.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<HkysUserProfileResponse>()
.setErrorCode(HyksErrorEnum.USER_NOT_FOUND.getErrorCode());
}
@Override
public HkysResponse<UpdateUserCoinResponse> 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<UpdateUserCoinResponse>()
.setErrorCode(HyksErrorEnum.AUTHENTICATION_FAILED.getErrorCode());
}
return new HkysResponse<HkysUserProfileResponse>()
.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<UpdateUserCoinResponse>()
.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<UpdateUserCoinResponse> updateUserCoin(HkysUserCoinUpdate param) {
long startTime = LocalDateTimeUtils.nowEpochMilli();
Long userId = param.longUid();
if (!UserCredential.checkToken(param.getToken(), userId)) {
return new HkysResponse<UpdateUserCoinResponse>()
.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<UpdateUserCoinResponse>()
.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<UpdateUserCoinResponse>()
.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<UpdateUserCoinResponse>()
.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<UpdateUserCoinResponse>()
.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<UpdateUserCoinResponse>()
.setErrorCode(4004);
}
} catch (Exception ex) {
log.error("ex:{}", Throwables.getStackTraceAsString(ex));
.setErrorCode(HyksErrorEnum.SERVER_ERROR.getErrorCode());
}
return new HkysResponse<UpdateUserCoinResponse>()
.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);
}
}

View File

@ -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<HotGameUserProfile> getUserProfile(HotGameUserProfileQuery query) {
if (!UserCredential.checkToken(query.getToken(), query.longUid())) {
return new HotGameResponse<HotGameUserProfile>()
.setErrorCode(HotGameErrorEnum.AUTHENTICATION_FAILED.getErrorCode());
}
UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO(
userProfileGateway.getByUserId(query.longUid()));
if (Objects.isNull(userProfile)) {
return new HotGameResponse<HotGameUserProfile>()
.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<HotGameUserProfile>()
.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<HotGameCoin> updateUserCoin(HotGameUserCoinUpdate param) {
long startTime = LocalDateTimeUtils.nowEpochMilli();
Long userId = param.longUid();
if (!UserCredential.checkToken(param.getToken(), userId)) {
return new HotGameResponse<HotGameCoin>()
.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<HotGameCoin>()
.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<HotGameCoin>()
@ -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<Object, Object> 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();
}
}

View File

@ -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<PennyAmount> 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<Object, Object> 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);
}
}

View File

@ -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<String, String> 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 幂等事件IDorderId / 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<Object, Object> 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())
);
}
}