|
|
@ -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]; |
|
|
|