Browse Source

优化积分校验方法

master
glx 1 month ago
parent
commit
99f93e9171
  1. 10
      src/main/java/com/youlai/boot/common/constant/CommonConstants.java
  2. 79
      src/main/java/com/youlai/boot/mini/service/impl/MiniPointRecordServiceImpl.java

10
src/main/java/com/youlai/boot/common/constant/CommonConstants.java

@ -1,5 +1,7 @@
package com.youlai.boot.common.constant; package com.youlai.boot.common.constant;
import java.time.format.DateTimeFormatter;
public class CommonConstants { public class CommonConstants {
//最多上传的图片数量 //最多上传的图片数量
@ -13,7 +15,11 @@ public class CommonConstants {
public static final String SIGN_DAY_KEY = "sign:user:%s:%s"; public static final String SIGN_DAY_KEY = "sign:user:%s:%s";
//连续签到天数缓存Key:占位符=用户ID //连续签到天数缓存Key:占位符=用户ID
public static final String SIGN_CONTINUOUS_KEY = "sign:continuous:%s"; public static final String SIGN_CONTINUOUS_KEY = "sign:continuous:%s";
//用户分享次数Key:占位符1=周期类型,占位符2=用户ID,占位符3=周期标识
public static final String SHARE_COUNT_KEY = "share:count:%s:%s:%s"; // 积分奖励周期计数Key:占位符1=业务前缀,占位符2=周期类型(all/day/week/month/year),占位符3=用户ID,占位符4=周期标识
public static final String REWARD_COUNT_KEY = "reward:count:%s:%s:%s:%s";
// 公用日期格式化器,避免重复创建
public static final DateTimeFormatter DATE_FORMATTER_DAY = DateTimeFormatter.ofPattern("yyyyMMdd");
public static final DateTimeFormatter DATE_FORMATTER_MONTH = DateTimeFormatter.ofPattern("yyyyMM");
} }

79
src/main/java/com/youlai/boot/mini/service/impl/MiniPointRecordServiceImpl.java

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.admin.service.PointManageService; import com.youlai.boot.admin.service.PointManageService;
import com.youlai.boot.common.constant.CommonConstants;
import com.youlai.boot.common.exception.MsgException; import com.youlai.boot.common.exception.MsgException;
import com.youlai.boot.mini.mapper.MiniPointAccountMapper; import com.youlai.boot.mini.mapper.MiniPointAccountMapper;
import com.youlai.boot.mini.mapper.MiniPointRecordMapper; import com.youlai.boot.mini.mapper.MiniPointRecordMapper;
@ -28,6 +29,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters; import java.time.temporal.TemporalAdjusters;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Service @Service
@ -54,7 +56,7 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe
* @return 校验结果[是否允许, 奖励积分, 计数Key, 过期天数, 周期类型, 超限提示, Redis是否正常] * @return 校验结果[是否允许, 奖励积分, 计数Key, 过期天数, 周期类型, 超限提示, Redis是否正常]
*/ */
private Object[] checkPeriodLimit(String ruleCode, Long userId, String bizPrefix) { private Object[] checkPeriodLimit(String ruleCode, Long userId, String bizPrefix) {
// 1. 查询规则 // 1. 查询规则(status=false为启用状态)
MiniPointRule rule = pointManageService.getOne(new LambdaQueryWrapper<MiniPointRule>() MiniPointRule rule = pointManageService.getOne(new LambdaQueryWrapper<MiniPointRule>()
.eq(MiniPointRule::getRuleCode, ruleCode) .eq(MiniPointRule::getRuleCode, ruleCode)
.eq(MiniPointRule::getStatus, false) .eq(MiniPointRule::getStatus, false)
@ -62,56 +64,40 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe
if (rule == null || rule.getPoints() <= 0) { if (rule == null || rule.getPoints() <= 0) {
return new Object[]{false, 0, null, 0L, null, "活动暂未开放", true}; return new Object[]{false, 0, null, 0L, null, "活动暂未开放", true};
} }
int rewardPoint = rule.getPoints(); int rewardPoint = rule.getPoints(); //积分值
String limitPeriod = rule.getLimitPeriod(); String limitPeriod = rule.getLimitPeriod(); //限制周期
int limitCount = rule.getLimitCount(); int limitCount = rule.getLimitCount(); //周期内限制次数
LocalDate now = LocalDate.now(); LocalDate now = LocalDate.now();
// 2. 分情况处理周期逻辑 // 完全无限制场景:不做次数校验
if ("ALL".equals(limitPeriod)) {
// ALL周期:永久限制次数,无时间范围,适合注册送分这类一生一次的奖励
String countKey = String.format("reward:count:%s:all:%s:all", bizPrefix, userId);
long currentCount = 0;
boolean redisNormal = true;
try {
currentCount = Optional.ofNullable(redisTemplate.opsForValue().get(countKey)).map(Long::parseLong).orElse(0L);
} catch (Exception e) {
redisNormal = false;
log.warn("Redis不可用,触发数据库计数兜底,ruleCode={}, userId={}", ruleCode, userId, e);
currentCount = countRecordByPeriod(userId, bizPrefix, limitPeriod, now);
}
// 校验永久次数上限
if (currentCount >= limitCount) {
return new Object[]{false, 0, null, 0L, limitPeriod, "该奖励仅可领取" + limitCount + "次", redisNormal};
}
// ALL周期缓存设置10年过期,足够覆盖用户生命周期
return new Object[]{true, rewardPoint, countKey, 3650L, limitPeriod, null, redisNormal};
}
// limit_period = null 才是完全无限制,不做任何次数校验
if (limitPeriod == null) { if (limitPeriod == null) {
return new Object[]{true, rewardPoint, null, 0L, limitPeriod, null, true}; return new Object[]{true, rewardPoint, null, 0L, limitPeriod, null, true};
} }
// 3. 周期计算 // 2. 统一处理所有周期
String periodKey; String periodKey;
long expireDays = 0; long expireDays = 0;
String limitTip = ""; String limitTip = "";
switch (limitPeriod) { switch (limitPeriod) {
case "ALL":
periodKey = "all";
expireDays = 3650L; // 10年过期,覆盖用户生命周期
limitTip = "该奖励仅可领取" + limitCount + "次";
break;
case "DAY": case "DAY":
periodKey = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")); periodKey = now.format(CommonConstants.DATE_FORMATTER_DAY);
expireDays = 1L; expireDays = 1L;
limitTip = "今日奖励已达上限,请明天再来"; limitTip = "今日奖励已达上限,请明天再来";
break; break;
case "WEEK": case "WEEK":
//用本周一日期作为周期key,解决跨年周计数分裂问题
LocalDate monday = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); LocalDate monday = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
periodKey = monday.format(DateTimeFormatter.ofPattern("yyyyMMdd")); periodKey = monday.format(CommonConstants.DATE_FORMATTER_DAY);
expireDays = 8L; expireDays = 8L; // 多留1天避免时区/临界点问题
limitTip = "本周奖励已达上限,请下周再来"; limitTip = "本周奖励已达上限,请下周再来";
break; break;
case "MONTH": case "MONTH":
periodKey = now.format(DateTimeFormatter.ofPattern("yyyyMM")); periodKey = now.format(CommonConstants.DATE_FORMATTER_MONTH);
expireDays = 32L; expireDays = 32L;
limitTip = "本月奖励已达上限,请下月再来"; limitTip = "本月奖励已达上限,请下月再来";
break; break;
@ -124,22 +110,43 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe
return new Object[]{true, rewardPoint, null, 0L, limitPeriod, null, true}; return new Object[]{true, rewardPoint, null, 0L, limitPeriod, null, true};
} }
// 4. 计数校验:优先读Redis,异常时走数据库兜底 // 3. 统一计数校验逻辑
String countKey = String.format("reward:count:%s:%s:%s:%s", bizPrefix, limitPeriod.toLowerCase(), userId, periodKey); String countKey = String.format(CommonConstants.REWARD_COUNT_KEY, bizPrefix, limitPeriod.toLowerCase(), userId, periodKey);
long currentCount = 0; long currentCount = 0;
boolean redisNormal = true; boolean redisNormal = true;
try { try {
currentCount = Optional.ofNullable(redisTemplate.opsForValue().get(countKey)).map(Long::parseLong).orElse(0L); currentCount = Optional.ofNullable(redisTemplate.opsForValue().get(countKey))
.map(Long::parseLong)
.orElse(0L);
} catch (Exception e) { } catch (Exception e) {
// Redis异常,走数据库兜底
redisNormal = false; redisNormal = false;
log.warn("Redis不可用,触发数据库计数兜底,ruleCode={}, userId={}", ruleCode, userId, e); log.warn("Redis不可用,触发数据库计数兜底,ruleCode={}, userId={}", ruleCode, userId, e);
// 走数据库查询准确计数
currentCount = countRecordByPeriod(userId, bizPrefix, limitPeriod, now); currentCount = countRecordByPeriod(userId, bizPrefix, limitPeriod, now);
// 异步回写Redis,不阻塞主流程,处理并发覆盖问题
long finalCurrentCount = currentCount;
long finalExpireDays = expireDays;
CompletableFuture.runAsync(() -> {
try {
// 用setIfAbsent避免Redis恢复期间已有新请求更新了计数导致覆盖
redisTemplate.opsForValue().setIfAbsent(countKey, String.valueOf(finalCurrentCount), finalExpireDays, TimeUnit.DAYS);
log.info("Redis恢复,回写周期计数成功,key={}, count={}", countKey, finalCurrentCount);
} catch (Exception ex) {
// 回写失败只打日志,不影响主业务
log.warn("Redis回写计数失败,key={}", countKey, ex);
} }
});
}
// 计数超限校验
if (currentCount >= limitCount) { if (currentCount >= limitCount) {
return new Object[]{false, 0, null, 0L, limitPeriod, limitTip, redisNormal}; return new Object[]{false, 0, null, 0L, limitPeriod, limitTip, redisNormal};
} }
// 返回顺序完全和原来一致,不影响现有调用方
return new Object[]{true, rewardPoint, countKey, expireDays, limitPeriod, null, redisNormal}; return new Object[]{true, rewardPoint, countKey, expireDays, limitPeriod, null, redisNormal};
} }
@ -191,7 +198,7 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Integer giveShareReward(Long userId) { public Integer giveShareReward(Long userId) {
// 调用通用周期校验方法 // 调用通用周期校验方法
Object[] checkResult = checkPeriodLimit("SHARE_CONTENT", userId, "share"); Object[] checkResult = checkPeriodLimit("SHARE_CONTENT", userId, "share_content");
boolean allow = (boolean) checkResult[0]; boolean allow = (boolean) checkResult[0];
int rewardPoint = (int) checkResult[1]; int rewardPoint = (int) checkResult[1];
String countKey = (String) checkResult[2]; String countKey = (String) checkResult[2];

Loading…
Cancel
Save