|
|
@ -19,6 +19,7 @@ import com.youlai.boot.mini.service.MiniPointAccountService; |
|
|
import com.youlai.boot.mini.service.MiniPointRecordService; |
|
|
import com.youlai.boot.mini.service.MiniPointRecordService; |
|
|
import lombok.RequiredArgsConstructor; |
|
|
import lombok.RequiredArgsConstructor; |
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
|
|
import org.springframework.dao.DuplicateKeyException; |
|
|
import org.springframework.data.redis.core.StringRedisTemplate; |
|
|
import org.springframework.data.redis.core.StringRedisTemplate; |
|
|
import org.springframework.stereotype.Service; |
|
|
import org.springframework.stereotype.Service; |
|
|
import org.springframework.transaction.annotation.Transactional; |
|
|
import org.springframework.transaction.annotation.Transactional; |
|
|
@ -50,17 +51,18 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 简单封装通用周期校验 |
|
|
* 简单封装通用周期校验 |
|
|
* @param ruleCode 规则编码 |
|
|
* |
|
|
* @param userId 用户ID |
|
|
* @param ruleCode 规则编码 |
|
|
|
|
|
* @param userId 用户ID |
|
|
* @param bizPrefix 业务前缀,避免不同业务Key冲突 |
|
|
* @param bizPrefix 业务前缀,避免不同业务Key冲突 |
|
|
* @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. 查询规则(status=false为启用状态)
|
|
|
// 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) |
|
|
.last("LIMIT 1")); |
|
|
.last("LIMIT 1")); |
|
|
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}; |
|
|
} |
|
|
} |
|
|
@ -117,8 +119,8 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
currentCount = Optional.ofNullable(redisTemplate.opsForValue().get(countKey)) |
|
|
currentCount = Optional.ofNullable(redisTemplate.opsForValue().get(countKey)) |
|
|
.map(Long::parseLong) |
|
|
.map(Long::parseLong) |
|
|
.orElse(0L); |
|
|
.orElse(0L); |
|
|
} catch (Exception e) { |
|
|
} catch (Exception e) { |
|
|
redisNormal = false; |
|
|
redisNormal = false; |
|
|
log.warn("Redis不可用,触发数据库计数兜底,ruleCode={}, userId={}", ruleCode, userId, e); |
|
|
log.warn("Redis不可用,触发数据库计数兜底,ruleCode={}, userId={}", ruleCode, userId, e); |
|
|
@ -157,9 +159,9 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe |
|
|
// ALL周期:统计该用户该业务类型的所有流水,无时间范围
|
|
|
// ALL周期:统计该用户该业务类型的所有流水,无时间范围
|
|
|
if ("ALL".equals(limitPeriod)) { |
|
|
if ("ALL".equals(limitPeriod)) { |
|
|
return count(new LambdaQueryWrapper<MiniPointRecord>() |
|
|
return count(new LambdaQueryWrapper<MiniPointRecord>() |
|
|
.eq(MiniPointRecord::getUserId, userId) |
|
|
.eq(MiniPointRecord::getUserId, userId) |
|
|
.eq(MiniPointRecord::getBizType, bizType.toUpperCase()) |
|
|
.eq(MiniPointRecord::getBizType, bizType.toUpperCase()) |
|
|
.eq(MiniPointRecord::getDeleted, 0)); |
|
|
.eq(MiniPointRecord::getDeleted, 0)); |
|
|
} |
|
|
} |
|
|
LocalDateTime startTime, endTime; |
|
|
LocalDateTime startTime, endTime; |
|
|
switch (limitPeriod) { |
|
|
switch (limitPeriod) { |
|
|
@ -187,11 +189,11 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe |
|
|
} |
|
|
} |
|
|
// 统计当前周期内该业务类型的流水数量
|
|
|
// 统计当前周期内该业务类型的流水数量
|
|
|
return count(new LambdaQueryWrapper<MiniPointRecord>() |
|
|
return count(new LambdaQueryWrapper<MiniPointRecord>() |
|
|
.eq(MiniPointRecord::getUserId, userId) |
|
|
.eq(MiniPointRecord::getUserId, userId) |
|
|
.eq(MiniPointRecord::getBizType, bizType.toUpperCase()) // 和流水的biz_type保持一致
|
|
|
.eq(MiniPointRecord::getBizType, bizType.toUpperCase()) // 和流水的biz_type保持一致
|
|
|
.ge(MiniPointRecord::getCreateTime, startTime) |
|
|
.ge(MiniPointRecord::getCreateTime, startTime) |
|
|
.le(MiniPointRecord::getCreateTime, endTime) |
|
|
.le(MiniPointRecord::getCreateTime, endTime) |
|
|
.eq(MiniPointRecord::getDeleted, 0)); |
|
|
.eq(MiniPointRecord::getDeleted, 0)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@Override |
|
|
@Override |
|
|
@ -233,82 +235,6 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe |
|
|
return rewardPoint; |
|
|
return rewardPoint; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
@Transactional(rollbackFor = Exception.class) |
|
|
|
|
|
public Integer deductAiGenerateImagePoint(Long userId, String taskId) { |
|
|
|
|
|
try { |
|
|
|
|
|
// 1. 幂等校验:同一个任务ID只能扣一次费,防止重试重复扣费
|
|
|
|
|
|
String idempotentKey = String.format("reward:ai_image:idempotent:%s", taskId); |
|
|
|
|
|
boolean alreadyDeducted = false; |
|
|
|
|
|
try { |
|
|
|
|
|
alreadyDeducted = Boolean.TRUE.equals(redisTemplate.hasKey(idempotentKey)); |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
// Redis异常查数据库兜底:查bizId等于taskId的流水
|
|
|
|
|
|
long count = count(new LambdaQueryWrapper<MiniPointRecord>() |
|
|
|
|
|
.eq(MiniPointRecord::getBizType, "AI_GENERATE_IMAGE") |
|
|
|
|
|
.eq(MiniPointRecord::getBizId, taskId) |
|
|
|
|
|
.eq(MiniPointRecord::getDeleted, 0)); |
|
|
|
|
|
alreadyDeducted = count > 0; |
|
|
|
|
|
} |
|
|
|
|
|
if (alreadyDeducted) { |
|
|
|
|
|
throw new MsgException("任务已处理"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 2. 调用通用周期校验,规则编码AI_GENERATE_IMAGE,业务前缀ai_generate_image
|
|
|
|
|
|
Object[] checkResult = checkPeriodLimit("AI_GENERATE_IMAGE", userId, "ai_generate_image"); |
|
|
|
|
|
boolean allow = (boolean) checkResult[0]; |
|
|
|
|
|
int deductPoint = (int) checkResult[1]; // 规则配置的扣除积分数,应该是负数
|
|
|
|
|
|
String countKey = (String) checkResult[2]; |
|
|
|
|
|
long expireDays = (long) checkResult[3]; |
|
|
|
|
|
String limitPeriod = (String) checkResult[4]; |
|
|
|
|
|
boolean redisNormal = (boolean) checkResult[6]; |
|
|
|
|
|
|
|
|
|
|
|
// 校验不通过(次数超限/规则未配置)或者扣除的积分>=0(规则配置错误),返回0
|
|
|
|
|
|
if (!allow || deductPoint >= 0) { |
|
|
|
|
|
throw new MsgException("积分规则配置错误"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 3. 检查用户积分是否足够
|
|
|
|
|
|
LambdaQueryWrapper<MiniPointAccount> pointAccountQuery = new LambdaQueryWrapper<>(); |
|
|
|
|
|
pointAccountQuery.eq(MiniPointAccount::getUserId, userId); |
|
|
|
|
|
pointAccountQuery.last("LIMIT 1"); |
|
|
|
|
|
MiniPointAccount account = miniPointAccountMapper.selectOne(pointAccountQuery); |
|
|
|
|
|
// 账户不存在积分就是0,扣除后为负说明积分不足
|
|
|
|
|
|
if (account == null || account.getPoints() + deductPoint < 0) { |
|
|
|
|
|
throw new MsgException("用户积分不足"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 4. 执行扣费
|
|
|
|
|
|
AdjustUserPointForm adjustForm = new AdjustUserPointForm(); |
|
|
|
|
|
adjustForm.setUserId(userId); |
|
|
|
|
|
adjustForm.setBizType("AI_GENERATE_IMAGE"); |
|
|
|
|
|
adjustForm.setChangeAmount(deductPoint); |
|
|
|
|
|
adjustForm.setBizId(taskId); |
|
|
|
|
|
// adjustForm.setRemark("AI生成图片扣费,任务ID:" + taskId);
|
|
|
|
|
|
pointManageService.adjustPoint(adjustForm); |
|
|
|
|
|
|
|
|
|
|
|
// 5. 更新周期计数缓存
|
|
|
|
|
|
if (countKey != null && expireDays > 0 && redisNormal) { |
|
|
|
|
|
redisTemplate.opsForValue().increment(countKey, 1); |
|
|
|
|
|
if ("DAY".equals(limitPeriod)) { |
|
|
|
|
|
redisTemplate.expire(countKey, 25, TimeUnit.HOURS); |
|
|
|
|
|
} else { |
|
|
|
|
|
redisTemplate.expire(countKey, expireDays, TimeUnit.DAYS); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 6. 写入幂等标记,存1年足够
|
|
|
|
|
|
if (redisNormal) { |
|
|
|
|
|
redisTemplate.opsForValue().set(idempotentKey, "1", 365, TimeUnit.DAYS); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return deductPoint; // 返回扣除的积分数,负数表示扣除成功
|
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
log.error("用户{}AI生成图片任务{}扣费失败", userId, taskId, e); |
|
|
|
|
|
return 0; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
@Override |
|
|
@Transactional(rollbackFor = Exception.class) |
|
|
@Transactional(rollbackFor = Exception.class) |
|
|
public Integer giveRegisterAnimalReward(Long userId, Long animalId) { |
|
|
public Integer giveRegisterAnimalReward(Long userId, Long animalId) { |
|
|
@ -352,4 +278,48 @@ public class MiniPointRecordServiceImpl extends ServiceImpl<MiniPointRecordMappe |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
@Transactional(rollbackFor = Exception.class) |
|
|
|
|
|
public Integer deductPoint(Long userId, String ruleCode, String bizId) { |
|
|
|
|
|
try { |
|
|
|
|
|
// 1. 幂等前置校验:查询是否已经处理过
|
|
|
|
|
|
long existCount = count(new LambdaQueryWrapper<MiniPointRecord>() |
|
|
|
|
|
.eq(MiniPointRecord::getBizType, ruleCode) |
|
|
|
|
|
.eq(MiniPointRecord::getBizId, bizId) |
|
|
|
|
|
.eq(MiniPointRecord::getDeleted, 0)); |
|
|
|
|
|
if (existCount > 0) { |
|
|
|
|
|
throw new MsgException("业务已处理,请勿重复请求"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 2. 查询积分规则并校验:必须存在、启用、且是扣费项目(points < 0)
|
|
|
|
|
|
MiniPointRule rule = pointManageService.getOne(new LambdaQueryWrapper<MiniPointRule>() |
|
|
|
|
|
.eq(MiniPointRule::getRuleCode, ruleCode) |
|
|
|
|
|
.eq(MiniPointRule::getStatus, false) // 启用状态
|
|
|
|
|
|
.last("LIMIT 1")); |
|
|
|
|
|
if (rule == null) { |
|
|
|
|
|
throw new MsgException("积分规则不存在"); |
|
|
|
|
|
} |
|
|
|
|
|
if (rule.getPoints() >= 0) { |
|
|
|
|
|
throw new MsgException("当前规则不是扣减类型,无法调用此接口"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 3. 执行扣减:uk_biz_id唯一索引兜底幂等,adjustPoint内置原子积分校验,不会超扣
|
|
|
|
|
|
AdjustUserPointForm adjustForm = new AdjustUserPointForm(); |
|
|
|
|
|
adjustForm.setUserId(userId); |
|
|
|
|
|
adjustForm.setBizType(ruleCode.toUpperCase()); |
|
|
|
|
|
adjustForm.setChangeAmount(rule.getPoints()); |
|
|
|
|
|
adjustForm.setBizId(bizId); |
|
|
|
|
|
pointManageService.adjustPoint(adjustForm); |
|
|
|
|
|
|
|
|
|
|
|
return rule.getPoints(); |
|
|
|
|
|
} catch (MsgException e) { |
|
|
|
|
|
throw e; |
|
|
|
|
|
} catch (DuplicateKeyException e) { |
|
|
|
|
|
throw new MsgException("重复请求"); |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
log.error("用户{}扣积分失败,规则:{},业务ID:{}", userId, ruleCode, bizId, e); |
|
|
|
|
|
throw new MsgException("积分扣除失败,请重试"); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|