热游 游戏新增用户VIP逻辑

This commit is contained in:
tianfeng 2026-01-07 18:20:06 +08:00
parent b6116ff994
commit e61c02a488
12 changed files with 396 additions and 41 deletions

View File

@ -0,0 +1,55 @@
package com.red.circle.other.inner.enums.game;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.math.BigDecimal;
/**
* VIP等级规则枚举按周区分
*/
@Getter
@AllArgsConstructor
public enum VipLevelRule {
// 第一周规则
WEEK1_V3(1, 3, new BigDecimal("500"), null),
WEEK1_V2(1, 2, new BigDecimal("100"), new BigDecimal("499")),
WEEK1_V1(1, 1, new BigDecimal("1"), new BigDecimal("99")),
// 第二周规则
WEEK2_V3(2, 3, new BigDecimal("700"), null),
WEEK2_V2(2, 2, new BigDecimal("500"), new BigDecimal("699")),
WEEK2_V1(2, 1, new BigDecimal("1"), new BigDecimal("499")),
// 第三周规则
WEEK3_V3(3, 3, new BigDecimal("1000"), null),
WEEK3_V2(3, 2, new BigDecimal("700"), new BigDecimal("999")),
WEEK3_V1(3, 1, new BigDecimal("1"), new BigDecimal("699"));
private final int week; // 1/2/3
private final int vipLevel; // VIP等级 0-3
private final BigDecimal minAmount; // 最低金额
private final BigDecimal maxAmount; // 最高金额null表示无上限
/**
* 根据周充值金额计算VIP等级
*/
public static int calculateVipLevel(int week, BigDecimal rechargeAmount) {
if (rechargeAmount == null || rechargeAmount.compareTo(BigDecimal.ZERO) <= 0) {
return 0; // 未充值
}
for (VipLevelRule rule : values()) {
if (rule.week == week) {
if (rechargeAmount.compareTo(rule.minAmount) >= 0) {
if (rule.maxAmount == null || rechargeAmount.compareTo(rule.maxAmount) <= 0) {
return rule.vipLevel;
}
}
}
}
return 0; // 默认V0
}
}

View File

@ -39,8 +39,11 @@ 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.entity.sys.GameListConfig;
import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService;
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.model.dto.user.UserProfileDTO;
import com.red.circle.tool.core.date.LocalDateTimeUtils;
import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
@ -58,10 +61,12 @@ import com.red.circle.wallet.inner.model.dto.WalletReceiptResDTO;
import com.red.circle.wallet.inner.model.enums.GoldOrigin;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.chrono.ChronoZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -73,11 +78,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* hotgame 游戏服务 实现.
*
* @author pengliang on 2023/4/17
*/
@Slf4j
@Service
@RequiredArgsConstructor
@ -95,11 +95,17 @@ public class HotGameServiceImpl implements HotGameService {
private final GameListCacheService gameListCacheService;
private final RankingActivityService rankingActivityService;
private final GameActivityService gameActivityService;
private final UserActivityRechargeService userActivityRechargeService;
private final GameVipLevelWeeklyService gameVipLevelWeeklyService;
@Value("${red-circle.game-rank.gameid}")
private String gameId;
@Value("${red-circle.game-rank.endTime}")
private String endTime;
private static final Long VIP_ACTIVITY_ID = 2005571533988298753L;
private static final String VIP_LEVEL_CACHE_KEY = "GAME:VIP:LEVEL:";
@Override
public HotGameResponse<HotGameUserProfile> getUserProfile(HotGameUserProfileQuery query) {
@ -120,11 +126,11 @@ public class HotGameServiceImpl implements HotGameService {
userProfile.getOriginSys()));
}
// Integer vipLevel = getVipLevel(query.getUid());
Integer vipLevel = getVipLevel(query.longUid());
return new HotGameResponse<HotGameUserProfile>()
.setErrorCode(HotGameErrorEnum.SUCCESS.getErrorCode())
.setData(HotGameUserProfile.builder()
.vipLevel(0)
.vipLevel(vipLevel)
.uid(Objects.toString(userProfile.getId()))
.nickname(userProfile.getUserNickname())
.avatar(userProfile.getUserAvatar())
@ -134,49 +140,50 @@ public class HotGameServiceImpl implements HotGameService {
);
}
public Integer getVipLevel(String userId) {
final String REDIS_KEY_PREFIX = "HOTGAME:";
final String REDIS_KEY_RATIO = REDIS_KEY_PREFIX + "RATIO";
final String REDIS_KEY_VIP = REDIS_KEY_PREFIX + "VIP";
public Integer getVipLevel(Long userId) {
try {
// 1. 检查大盘返奖比
Object ratioObj = redisService.redisTemplate().opsForValue().get(REDIS_KEY_RATIO);
BigDecimal ratio = BigDecimal.ZERO;
if (ratioObj != null) {
ratio = new BigDecimal(ratioObj.toString());
String cacheKey = VIP_LEVEL_CACHE_KEY + userId;
Object cachedLevel = redisService.redisTemplate().opsForValue().get(cacheKey);
if (cachedLevel != null) {
return Integer.parseInt(cachedLevel.toString());
}
if (ratio.compareTo(BigDecimal.valueOf(100)) > 0) {
LocalDate firstRechargeDate = userActivityRechargeService.getFirstRechargeDate(userId, VIP_ACTIVITY_ID);
if (firstRechargeDate == null) {
redisService.redisTemplate().opsForValue().set(cacheKey, 0, 1, TimeUnit.DAYS);
return 0;
}
// 2. 按VIP等级从高到低查询
Object vip3Obj = redisService.redisTemplate().opsForHash().get(REDIS_KEY_VIP, "VIP3");
if (vip3Obj != null) {
List<String> vip3Users = JSON.parseArray(vip3Obj.toString(), String.class);
if (vip3Users.contains(userId)) {
return 3;
}
}
LocalDate today = ZonedDateTimeAsiaRiyadhUtils.now().toLocalDate();
long daysBetween = ChronoUnit.DAYS.between(firstRechargeDate, today);
int currentWeek = (int) (daysBetween / 7) + 1;
Object vip2Obj = redisService.redisTemplate().opsForHash().get(REDIS_KEY_VIP, "VIP2");
if (vip2Obj != null) {
List<String> vip2Users = JSON.parseArray(vip2Obj.toString(), String.class);
if (vip2Users.contains(userId)) {
return 2;
}
}
int normalizedWeek = ((currentWeek - 1) % 3) + 1;
Object vip1Obj = redisService.redisTemplate().opsForHash().get(REDIS_KEY_VIP, "VIP1");
if (vip1Obj != null) {
List<String> vip1Users = JSON.parseArray(vip1Obj.toString(), String.class);
if (vip1Users.contains(userId)) {
return 1;
}
}
LocalDate weekStartDate = firstRechargeDate.plusWeeks(currentWeek - 1);
LocalDate weekEndDate = weekStartDate.plusDays(6);
int lastWeekLevel = gameVipLevelWeeklyService.getLastWeekFinalLevel(userId, VIP_ACTIVITY_ID, weekStartDate);
int startLevel = Math.max(0, lastWeekLevel - 1);
BigDecimal thisWeekRecharge = userActivityRechargeService.getRechargeAmountByDateRange(
userId, VIP_ACTIVITY_ID, weekStartDate, weekEndDate
);
int rechargeLevel = VipLevelRule.calculateVipLevel(normalizedWeek, thisWeekRecharge);
int finalLevel = Math.max(startLevel, rechargeLevel);
gameVipLevelWeeklyService.saveOrUpdateWeeklyLevel(
userId, VIP_ACTIVITY_ID, currentWeek, weekStartDate, weekEndDate, finalLevel, thisWeekRecharge
);
redisService.redisTemplate().opsForValue().set(cacheKey, finalLevel, 1, TimeUnit.DAYS);
return finalLevel;
return 0;
} catch (Exception e) {
System.out.println("获取VIP等级异常: " + e.getMessage());
log.error("获取VIP等级异常, userId:{}, error:{}", userId, e.getMessage(), e);
return 0;
}

View File

@ -56,4 +56,27 @@ public interface UserActivityRechargeDAO extends BaseDAO<UserActivityRecharge> {
* @return 排名从1开始
*/
Integer getUserRank(@Param("activityId") Long activityId, @Param("userId") Long userId);
/**
* 查询用户在活动中的首次充值日期
*
* @param userId 用户ID
* @param activityId 活动ID
* @return 首次充值日期
*/
String getFirstRechargeDate(@Param("userId") Long userId, @Param("activityId") Long activityId);
/**
* 查询用户在活动中指定日期范围内的充值总额
*
* @param userId 用户ID
* @param activityId 活动ID
* @param startDate 开始日期包含
* @param endDate 结束日期包含
* @return 充值总额
*/
BigDecimal getRechargeAmountByDateRange(@Param("userId") Long userId,
@Param("activityId") Long activityId,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
}

View File

@ -0,0 +1,18 @@
package com.red.circle.other.infra.database.rds.dao.game;
import com.red.circle.framework.mybatis.dao.BaseDAO;
import com.red.circle.other.infra.database.rds.entity.game.GameVipLevelWeekly;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
public interface GameVipLevelWeeklyDAO extends BaseDAO<GameVipLevelWeekly> {
GameVipLevelWeekly getByUserAndWeek(@Param("userId") Long userId,
@Param("activityId") Long activityId,
@Param("weekStartDate") LocalDate weekStartDate);
GameVipLevelWeekly getLastWeekLevel(@Param("userId") Long userId,
@Param("activityId") Long activityId,
@Param("currentWeekStartDate") LocalDate currentWeekStartDate);
}

View File

@ -0,0 +1,56 @@
package com.red.circle.other.infra.database.rds.entity.game;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.LocalDate;
/**
* 游戏VIP每周等级记录
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("game_vip_level_weekly")
public class GameVipLevelWeekly implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("user_id")
private Long userId;
@TableField("activity_id")
private Long activityId;
@TableField("week_number")
private Integer weekNumber;
@TableField("week_start_date")
private LocalDate weekStartDate;
@TableField("week_end_date")
private LocalDate weekEndDate;
@TableField("final_level")
private Integer finalLevel;
@TableField("recharge_amount")
private BigDecimal rechargeAmount;
@TableField("created_at")
private Timestamp createdAt;
@TableField("updated_at")
private Timestamp updatedAt;
}

View File

@ -3,6 +3,7 @@ package com.red.circle.other.infra.database.rds.service.activity;
import com.red.circle.order.inner.model.enums.MonthlyRechargeType;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 用户活动充值记录服务
@ -26,4 +27,24 @@ public interface UserActivityRechargeService {
* @return 总金额
*/
BigDecimal getUserTotalAmount(Long userId, Long activityId);
/**
* 查询用户在活动中的首次充值日期
*
* @param userId 用户ID
* @param activityId 活动ID
* @return 首次充值日期
*/
LocalDate getFirstRechargeDate(Long userId, Long activityId);
/**
* 查询用户在活动中指定日期范围内的充值总额
*
* @param userId 用户ID
* @param activityId 活动ID
* @param startDate 开始日期包含
* @param endDate 结束日期包含
* @return 充值总额
*/
BigDecimal getRechargeAmountByDateRange(Long userId, Long activityId, LocalDate startDate, LocalDate endDate);
}

View File

@ -4,6 +4,7 @@ import com.red.circle.order.inner.model.enums.MonthlyRechargeType;
import com.red.circle.other.infra.database.rds.dao.activity.UserActivityRechargeDAO;
import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService;
import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
import com.red.circle.tool.core.text.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -47,4 +48,20 @@ public class UserActivityRechargeServiceImpl implements UserActivityRechargeServ
public BigDecimal getUserTotalAmount(Long userId, Long activityId) {
return userActivityRechargeDAO.getUserTotalAmount(userId, activityId);
}
@Override
public LocalDate getFirstRechargeDate(Long userId, Long activityId) {
String dateStr = userActivityRechargeDAO.getFirstRechargeDate(userId, activityId);
if (StringUtils.isBlank(dateStr)) {
return null;
}
return LocalDate.parse(dateStr);
}
@Override
public BigDecimal getRechargeAmountByDateRange(Long userId, Long activityId, LocalDate startDate, LocalDate endDate) {
String startDateStr = startDate.toString();
String endDateStr = endDate.toString();
return userActivityRechargeDAO.getRechargeAmountByDateRange(userId, activityId, startDateStr, endDateStr);
}
}

View File

@ -0,0 +1,13 @@
package com.red.circle.other.infra.database.rds.service.game;
import java.math.BigDecimal;
import java.time.LocalDate;
public interface GameVipLevelWeeklyService {
Integer getLastWeekFinalLevel(Long userId, Long activityId, LocalDate currentWeekStartDate);
void saveOrUpdateWeeklyLevel(Long userId, Long activityId, Integer weekNumber,
LocalDate weekStartDate, LocalDate weekEndDate,
Integer finalLevel, BigDecimal rechargeAmount);
}

View File

@ -0,0 +1,53 @@
package com.red.circle.other.infra.database.rds.service.game.impl;
import com.red.circle.other.infra.database.rds.dao.game.GameVipLevelWeeklyDAO;
import com.red.circle.other.infra.database.rds.entity.game.GameVipLevelWeekly;
import com.red.circle.other.infra.database.rds.service.game.GameVipLevelWeeklyService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.LocalDate;
@Slf4j
@Service
@RequiredArgsConstructor
public class GameVipLevelWeeklyServiceImpl implements GameVipLevelWeeklyService {
private final GameVipLevelWeeklyDAO gameVipLevelWeeklyDAO;
@Override
public Integer getLastWeekFinalLevel(Long userId, Long activityId, LocalDate currentWeekStartDate) {
GameVipLevelWeekly lastWeek = gameVipLevelWeeklyDAO.getLastWeekLevel(userId, activityId, currentWeekStartDate);
return lastWeek != null ? lastWeek.getFinalLevel() : 0;
}
@Override
public void saveOrUpdateWeeklyLevel(Long userId, Long activityId, Integer weekNumber,
LocalDate weekStartDate, LocalDate weekEndDate,
Integer finalLevel, BigDecimal rechargeAmount) {
GameVipLevelWeekly existing = gameVipLevelWeeklyDAO.getByUserAndWeek(userId, activityId, weekStartDate);
if (existing != null) {
existing.setFinalLevel(finalLevel);
existing.setRechargeAmount(rechargeAmount);
existing.setUpdatedAt(new Timestamp(System.currentTimeMillis()));
gameVipLevelWeeklyDAO.updateById(existing);
} else {
GameVipLevelWeekly record = new GameVipLevelWeekly();
record.setUserId(userId);
record.setActivityId(activityId);
record.setWeekNumber(weekNumber);
record.setWeekStartDate(weekStartDate);
record.setWeekEndDate(weekEndDate);
record.setFinalLevel(finalLevel);
record.setRechargeAmount(rechargeAmount);
Timestamp now = new Timestamp(System.currentTimeMillis());
record.setCreatedAt(now);
record.setUpdatedAt(now);
gameVipLevelWeeklyDAO.insert(record);
}
}
}

View File

@ -44,4 +44,21 @@
)
</select>
<select id="getFirstRechargeDate" resultType="java.lang.String">
SELECT MIN(recharge_date)
FROM user_activity_recharge
WHERE user_id = #{userId}
AND activity_id = #{activityId}
AND amount > 0
</select>
<select id="getRechargeAmountByDateRange" resultType="java.math.BigDecimal">
SELECT IFNULL(SUM(amount), 0)
FROM user_activity_recharge
WHERE user_id = #{userId}
AND activity_id = #{activityId}
AND recharge_date &gt;= #{startDate}
AND recharge_date &lt;= #{endDate}
</select>
</mapper>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.red.circle.other.infra.database.rds.dao.game.GameVipLevelWeeklyDAO">
<select id="getByUserAndWeek" resultType="com.red.circle.other.infra.database.rds.entity.game.GameVipLevelWeekly">
SELECT *
FROM game_vip_level_weekly
WHERE user_id = #{userId}
AND activity_id = #{activityId}
AND week_start_date = #{weekStartDate}
LIMIT 1
</select>
<select id="getLastWeekLevel" resultType="com.red.circle.other.infra.database.rds.entity.game.GameVipLevelWeekly">
SELECT *
FROM game_vip_level_weekly
WHERE user_id = #{userId}
AND activity_id = #{activityId}
AND week_start_date &lt; #{currentWeekStartDate}
ORDER BY week_start_date DESC
LIMIT 1
</select>
</mapper>

View File

@ -0,0 +1,50 @@
import com.red.circle.OtherServiceApplication;
import com.red.circle.order.inner.model.enums.MonthlyRechargeType;
import com.red.circle.other.app.service.game.HotGameServiceImpl;
import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService;
import com.red.circle.other.infra.database.rds.service.game.GameVipLevelWeeklyService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
@Slf4j
@SpringBootTest(classes = OtherServiceApplication.class)
@RunWith(SpringRunner.class)
public class GameVipLevelTest {
@Autowired
private HotGameServiceImpl hotGameService;
@Autowired
private UserActivityRechargeService userActivityRechargeService;
@Autowired
private GameVipLevelWeeklyService gameVipLevelWeeklyService;
private static final Long TEST_USER_ID = 1957345312961527809L;
private static final Long ACTIVITY_ID = 2005571533988298753L;
@Test
public void testGetVipLevel() {
Integer vipLevel = hotGameService.getVipLevel(TEST_USER_ID);
log.info("用户VIP等级: userId={}, vipLevel={}", TEST_USER_ID, vipLevel);
}
@Test
public void testRecordRecharge() {
userActivityRechargeService.recordRecharge(TEST_USER_ID, new BigDecimal("100"), MonthlyRechargeType.SHIPPING_AGENT);
log.info("记录充值完成: userId={}, amount=100", TEST_USER_ID);
}
@Test
public void testGetVipLevelAfterRecharge() {
userActivityRechargeService.recordRecharge(TEST_USER_ID, new BigDecimal("500"), MonthlyRechargeType.SHIPPING_AGENT);
Integer vipLevel = hotGameService.getVipLevel(TEST_USER_ID);
log.info("充值后VIP等级: userId={}, vipLevel={}", TEST_USER_ID, vipLevel);
}
}