diff --git a/src/main/java/com/sqx/common/annotation/Debounce.java b/src/main/java/com/sqx/common/annotation/Debounce.java new file mode 100644 index 00000000..fa918c50 --- /dev/null +++ b/src/main/java/com/sqx/common/annotation/Debounce.java @@ -0,0 +1,24 @@ +package com.sqx.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * @author ww + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Debounce { + // 防抖时间间隔,默认2秒 + long interval() default 2000; + // 时间单位,默认毫秒 + TimeUnit timeUnit() default TimeUnit.MILLISECONDS; + + // 用于指定基于的入参表达式,为空时对整个方法防抖 + // 格式为 #入参键.值 例如: #receive.id + // 多个参数 使用逗号进行拼接 例如: #receive.id,#receive.name + String value() default ""; +} diff --git a/src/main/java/com/sqx/common/aspect/AppApiMethodAspect.java b/src/main/java/com/sqx/common/aspect/AppApiMethodAspect.java index f6e9b9f1..488f7f7b 100644 --- a/src/main/java/com/sqx/common/aspect/AppApiMethodAspect.java +++ b/src/main/java/com/sqx/common/aspect/AppApiMethodAspect.java @@ -5,6 +5,7 @@ import com.sqx.common.utils.DateUtils; import com.sqx.common.utils.HttpContextUtils; import com.sqx.common.utils.IPUtils; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -32,24 +33,27 @@ public class AppApiMethodAspect { } @Around("pkg()") -// @SuppressWarnings("unchecked") public Object around(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); - // 执行被拦截的方法 - Object result = pjp.proceed(); - //请求的参数 + //避免回填 Object[] args = pjp.getArgs(); String params = new Gson().toJson(args); + + // 执行被拦截的方法 + Object result = pjp.proceed(); + + //请求的参数 String resultJson = new Gson().toJson(result); - //获取request + HttpServletRequest request = HttpContextUtils.getHttpServletRequest(); long end = System.currentTimeMillis(); - - log.info("\n>>>>>> {} {}\n>>>>>> IP: {} \n>>>>>> execute time:{}\n>>>>>> Request: {}\n>>>>>> Response: {}", - request.getMethod(), request.getRequestURL(), IPUtils.getIpAddr(request),end-start, - params, - resultJson - ); + if(StringUtils.isNotBlank(resultJson) && !"null".equals(resultJson)){ + log.info("\n>>>>>> {} {}\n>>>>>> IP: {} \n>>>>>> execute time:{}ms \n>>>>>> Request: {}\n>>>>>> Response: {}", + request.getMethod(), request.getRequestURL(), IPUtils.getIpAddr(request),end-start, + params, + resultJson + ); + } return result; } } diff --git a/src/main/java/com/sqx/common/aspect/DebounceAspect.java b/src/main/java/com/sqx/common/aspect/DebounceAspect.java new file mode 100644 index 00000000..0b3bb99b --- /dev/null +++ b/src/main/java/com/sqx/common/aspect/DebounceAspect.java @@ -0,0 +1,117 @@ +package com.sqx.common.aspect; + +import com.sqx.common.annotation.Debounce; +import com.sqx.common.utils.SpelUtil; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + + +/** + * 防抖切面 + * @author ww + */ +@Aspect +@Component +public class DebounceAspect { + + @Pointcut("@annotation(com.sqx.common.annotation.Debounce)") + public void logPointCut() { + + } + + // 用于存储基于方法和入参情况的上次执行时间,结构为:方法签名 -> (入参值 -> 上次执行时间) + private static final ConcurrentHashMap> executionTimeMap = new ConcurrentHashMap<>(); + private static final ReentrantLock lock = new ReentrantLock(); + + @Around("logPointCut()") + public Object aroundDebounce(ProceedingJoinPoint joinPoint) throws Throwable { + cleanExpiredRecords(); + String methodSignature = joinPoint.getSignature().toLongString(); + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Debounce annotation = signature.getMethod().getAnnotation(Debounce.class); + long interval = annotation.interval(); + TimeUnit timeUnit = annotation.timeUnit(); + String value = annotation.value(); + if (StringUtils.isBlank(value)) { + // 没有指定入参表达式,按照整个方法进行防抖 + return debounceForWholeMethod(joinPoint, methodSignature, interval, timeUnit); + } + String[] split = value.split(","); + StringBuilder values = new StringBuilder(); + for (String str : split) { + values.append(SpelUtil.generateKeyBySpEL(str, joinPoint)); + } + // 解析入参表达式,获取对应入参的值 + + if (StringUtils.isBlank(values.toString())) { + // 如果解析失败或值为null,按照整个方法进行防抖 + return debounceForWholeMethod(joinPoint, methodSignature, interval, timeUnit); + } + + // 根据方法签名和入参值进行防抖判断 + return debounceForSpecificValue(joinPoint, methodSignature, interval, timeUnit, values.toString()); + } + + private Object debounceForWholeMethod(ProceedingJoinPoint joinPoint, String methodSignature, long interval, TimeUnit timeUnit) throws Throwable { + ConcurrentHashMap methodExecutionTimeMap = executionTimeMap.computeIfAbsent(methodSignature, k -> new ConcurrentHashMap<>()); + long currentTime = System.currentTimeMillis(); + Long lastTime = methodExecutionTimeMap.get(methodSignature); + if (lastTime == null) { + // 如果不存在对应键值对,设置初始值,这里设置为一个表示很久之前的时间戳,比如0 + lastTime = 0L; + } + if (lastTime == null || currentTime - timeUnit.toMillis(interval) >= lastTime) { + // 满足防抖间隔,更新上次执行时间,并执行目标方法 + methodExecutionTimeMap.put(methodSignature, currentTime); + return joinPoint.proceed(); + } + // 在防抖间隔内,不执行目标方法,直接返回 + return null; + } + + private Object debounceForSpecificValue(ProceedingJoinPoint joinPoint, String methodSignature, long interval, TimeUnit timeUnit, Object targetValue) throws Throwable { + ConcurrentHashMap methodExecutionTimeMap = executionTimeMap.computeIfAbsent(methodSignature, k -> new ConcurrentHashMap<>()); + long currentTime = System.currentTimeMillis(); + Long lastTime = methodExecutionTimeMap.get(targetValue); + if (lastTime == null || currentTime - timeUnit.toMillis(interval) >= lastTime) { + // 满足防抖间隔,更新上次执行时间,并执行目标方法 + methodExecutionTimeMap.put(targetValue, currentTime); + return joinPoint.proceed(); + } + // 在防抖间隔内,不执行目标方法,直接返回 + return null; + } + + public void cleanExpiredRecords() { + long expirationTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(10); + lock.lock(); + try { + for (Entry> outerEntry : executionTimeMap.entrySet()) { + String methodSignature = outerEntry.getKey(); + ConcurrentHashMap innerMap = outerEntry.getValue(); + ConcurrentHashMap keysToRemove = new ConcurrentHashMap<>(); + for (Entry innerEntry : innerMap.entrySet()) { + if (innerEntry.getValue() < expirationTime) { + keysToRemove.put(innerEntry.getKey(), innerEntry.getValue()); + } + } + innerMap.keySet().removeAll(keysToRemove.keySet()); + if (innerMap.isEmpty()) { + executionTimeMap.remove(methodSignature); + } + } + } finally { + lock.unlock(); + } + } +} diff --git a/src/main/java/com/sqx/common/utils/SpelUtil.java b/src/main/java/com/sqx/common/utils/SpelUtil.java new file mode 100644 index 00000000..f1ccc0e6 --- /dev/null +++ b/src/main/java/com/sqx/common/utils/SpelUtil.java @@ -0,0 +1,52 @@ +package com.sqx.common.utils; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +public class SpelUtil { + /** + * 用于SpEL表达式解析. + */ + private static final SpelExpressionParser parser = new SpelExpressionParser(); + + /** + * 用于获取方法参数定义名字. + */ + private static final DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); + + /** + * 解析SpEL表达式 + * + * @param spELStr + * @param joinPoint + * @return + */ + public static String generateKeyBySpEL(String spELStr, ProceedingJoinPoint joinPoint) { + // 通过joinPoint获取被注解方法 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + Method method = methodSignature.getMethod(); + // 使用Spring的DefaultParameterNameDiscoverer获取方法形参名数组 + String[] paramNames = nameDiscoverer.getParameterNames(method); +// // 解析过后的Spring表达式对象 + Expression expression = parser.parseExpression(spELStr); + // Spring的表达式上下文对象 + EvaluationContext context = new StandardEvaluationContext(); + // 通过joinPoint获取被注解方法的形参 + Object[] args = joinPoint.getArgs(); + // 给上下文赋值 + for (int i = 0; i < args.length; i++) { + context.setVariable(paramNames[i], args[i]); + } + if(expression.getValue(context)==null){ + return ""; + } + return expression.getValue(context).toString(); + } +} diff --git a/src/main/java/com/sqx/modules/course/controller/app/AppCourseController.java b/src/main/java/com/sqx/modules/course/controller/app/AppCourseController.java index 9ebc2bbf..8d5ad0f6 100644 --- a/src/main/java/com/sqx/modules/course/controller/app/AppCourseController.java +++ b/src/main/java/com/sqx/modules/course/controller/app/AppCourseController.java @@ -98,14 +98,12 @@ public class AppCourseController extends AbstractController { return courseService.selectWxVideoUrl(wxCourseDetailsIds); } - @Login @GetMapping("/viewCourse") @ApiOperation("查看短剧") - public Result viewCourse(@RequestAttribute Long userId, - @ApiParam("短剧id") Long courseId, + public Result viewCourse(@ApiParam("短剧id") Long courseId, @ApiParam("剧集id") Long courseDetailsId, @ApiParam("统计类型:start 开始,end 结束") String type) { - return courseService.viewCourse(userId, courseId, courseDetailsId, type); + return courseService.viewCourse(courseId, courseDetailsId, type); } } diff --git a/src/main/java/com/sqx/modules/course/service/CourseService.java b/src/main/java/com/sqx/modules/course/service/CourseService.java index 6b006a68..85932b6d 100644 --- a/src/main/java/com/sqx/modules/course/service/CourseService.java +++ b/src/main/java/com/sqx/modules/course/service/CourseService.java @@ -59,7 +59,7 @@ public interface CourseService extends IService { Result courseListExcelIn(MultipartFile file) throws IOException; - Result viewCourse(Long userId, Long courseId, Long courseDetailsId, String type); + Result viewCourse(Long courseId, Long courseDetailsId, String type); Result getRedEnvelopeTips(Long userId); diff --git a/src/main/java/com/sqx/modules/course/service/impl/CourseServiceImpl.java b/src/main/java/com/sqx/modules/course/service/impl/CourseServiceImpl.java index 45712c34..f3a0ef1e 100644 --- a/src/main/java/com/sqx/modules/course/service/impl/CourseServiceImpl.java +++ b/src/main/java/com/sqx/modules/course/service/impl/CourseServiceImpl.java @@ -1235,7 +1235,7 @@ public class CourseServiceImpl extends ServiceImpl implements } @Override - public Result viewCourse(Long userId, Long courseId, Long courseDetailsId, String type) { + public Result viewCourse(Long courseId, Long courseDetailsId, String type) { Course course = baseMapper.selectById(courseId); if(course==null){ return Result.error("短剧不存在"); diff --git a/src/main/java/com/sqx/modules/discSpinning/controller/DiscSpinningController.java b/src/main/java/com/sqx/modules/discSpinning/controller/DiscSpinningController.java index 626b2ed4..ce6abba3 100644 --- a/src/main/java/com/sqx/modules/discSpinning/controller/DiscSpinningController.java +++ b/src/main/java/com/sqx/modules/discSpinning/controller/DiscSpinningController.java @@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.google.common.util.concurrent.RateLimiter; +import com.sqx.common.annotation.Debounce; import com.sqx.common.utils.DateUtils; import com.sqx.common.utils.Result; import com.sqx.modules.app.annotation.Login; @@ -42,6 +43,7 @@ import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; @Slf4j @RestController @@ -59,9 +61,6 @@ public class DiscSpinningController { private final CashOutService cashOutService; private final TaskCenterService taskCenterService; - // 以id为键,对应的RateLimiter实例为值,用于不同id的防抖控制 - private static final ConcurrentHashMap rateLimiterMap = new ConcurrentHashMap<>(); - @Autowired public DiscSpinningController(CommonInfoService commonRepository, DiscSpinningService discSpinningService, @@ -196,29 +195,23 @@ public class DiscSpinningController { draws(amount, orderId, userId, maps == null || maps.get("source") == null ? "order" : maps.get("source").toString())); } - @PostMapping("/app/discSpinning/receive") @ApiOperation("大转盘奖项领取") + @Debounce(interval = 3000, value = "#receive.id") + @PostMapping("/app/discSpinning/receive") public Result receive(@RequestBody DiscSpinningRecord receive) { - // 每秒允许0.6次操作 - RateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(receive.getId(), k -> RateLimiter.create(0.6)); - if (rateLimiter.tryAcquire()) { - CompletableFuture.runAsync(() -> { - DiscSpinningRecord record = recordService.getById(receive.getId()); - CompletableFuture.runAsync(() -> { - receiveAsync(record); - }); - }); - return Result.success(); - } - return Result.error("操作过于频繁,请稍后再试"); + DiscSpinningRecord record = recordService.getById(receive.getId()); + CompletableFuture.runAsync(() -> { + receiveAsync(record); + }); + return Result.success(); } @Transactional public void receiveAsync(DiscSpinningRecord receive) { UserEntity userInfo = userService.queryByUserId(receive.getUserId()); UserMoneyDetails userMoneyDetails = new UserMoneyDetails( - receive.getUserId(),null,null,"[现金大转盘]",5,1,2, - receive.getNumber(),"现金红包奖励" + receive.getNumber() + "元"); + receive.getUserId(), null, null, "[现金大转盘]", 5, 1, 2, + receive.getNumber(), "现金红包奖励" + receive.getNumber() + "元"); userMoneyDetailsService.save(userMoneyDetails); receive.setTarget("2"); receive.setTargetId(userMoneyDetails.getId()); @@ -247,8 +240,8 @@ public class DiscSpinningController { cashOut.setCreateAt(DateUtil.now()); UserMoneyDetails userMoneyDetails = new UserMoneyDetails( - userInfo.getUserId(),null,null,"[现金大转盘]",4,2,1, - new BigDecimal(money),"现金红包自动提现" + money + "元"); + userInfo.getUserId(), null, null, "[现金大转盘]", 4, 2, 1, + new BigDecimal(money), "现金红包自动提现" + money + "元"); userMoneyDetailsService.save(userMoneyDetails); //减去余额 钱 userMoneyService.updateAmount(2, userInfo.getUserId(), money); diff --git a/src/main/java/com/sqx/modules/pay/controller/app/AppCashController.java b/src/main/java/com/sqx/modules/pay/controller/app/AppCashController.java index 334b31d3..b194aaea 100644 --- a/src/main/java/com/sqx/modules/pay/controller/app/AppCashController.java +++ b/src/main/java/com/sqx/modules/pay/controller/app/AppCashController.java @@ -1,6 +1,7 @@ package com.sqx.modules.pay.controller.app; +import com.sqx.common.annotation.Debounce; import com.sqx.common.utils.PageUtils; import com.sqx.common.utils.Result; import com.sqx.modules.app.annotation.Login; @@ -44,6 +45,7 @@ public class AppCashController { @Login @GetMapping(value = "/withdraw") + @Debounce(interval = 3000, value = "#userId") @ApiOperation("发起提现 余额 金钱") public Result withdraw(@RequestAttribute("userId") Long userId, Double amount) { return cashOutService.withdraw(userId, amount, false); diff --git a/src/main/java/com/sqx/modules/pay/controller/app/WuyouController.java b/src/main/java/com/sqx/modules/pay/controller/app/WuyouController.java index 88103137..c82859e8 100644 --- a/src/main/java/com/sqx/modules/pay/controller/app/WuyouController.java +++ b/src/main/java/com/sqx/modules/pay/controller/app/WuyouController.java @@ -124,6 +124,7 @@ public class WuyouController { if (cashOut != null) { if ("2".equals(notifyDto.getStatus())) { cashOut.setState(1); + cashOut.setOutAt(DateUtil.now()); } else { cashOut.setState(2); cashOut.setRefund(notifyDto.getMsg()); diff --git a/src/main/java/com/sqx/modules/taskCenter/controller/TaskCenterController.java b/src/main/java/com/sqx/modules/taskCenter/controller/TaskCenterController.java index 3a283903..13329f49 100644 --- a/src/main/java/com/sqx/modules/taskCenter/controller/TaskCenterController.java +++ b/src/main/java/com/sqx/modules/taskCenter/controller/TaskCenterController.java @@ -2,6 +2,7 @@ package com.sqx.modules.taskCenter.controller; import cn.hutool.core.date.DateUtil; +import com.sqx.common.annotation.Debounce; import com.sqx.modules.app.annotation.Login; import com.sqx.modules.taskCenter.entity.TaskCenter; import com.sqx.modules.taskCenter.service.TaskCenterService; @@ -80,6 +81,7 @@ public class TaskCenterController { @ApiImplicitParam(name = "id", value = "任务id", dataTypeClass = Long.class), }) @ApiOperation("App 任务中心 领取") + @Debounce(interval = 1000, value = "#userId") public Result taskReceive(@ApiIgnore @RequestAttribute("userId") Long userId,Long id) { return taskCenterService.taskReceive(userId,id); } diff --git a/src/main/java/com/sqx/modules/taskCenter/entity/TaskCenter.java b/src/main/java/com/sqx/modules/taskCenter/entity/TaskCenter.java index 143cb4d7..0ac5061a 100644 --- a/src/main/java/com/sqx/modules/taskCenter/entity/TaskCenter.java +++ b/src/main/java/com/sqx/modules/taskCenter/entity/TaskCenter.java @@ -53,6 +53,8 @@ public class TaskCenter extends Model { @ApiModelProperty("是否开启 0否1是") private Integer shows; + @TableField(exist = false) + private Integer discNumber; @TableField(exist = false) private boolean disabled = true; diff --git a/src/main/java/com/sqx/modules/taskCenter/service/impl/TaskCenterServiceImpl.java b/src/main/java/com/sqx/modules/taskCenter/service/impl/TaskCenterServiceImpl.java index 4ff0ad56..473b6c98 100644 --- a/src/main/java/com/sqx/modules/taskCenter/service/impl/TaskCenterServiceImpl.java +++ b/src/main/java/com/sqx/modules/taskCenter/service/impl/TaskCenterServiceImpl.java @@ -139,29 +139,19 @@ public class TaskCenterServiceImpl extends ServiceImpl signWrapper = new QueryWrapper<>(); + signWrapper.eq("user_id", userId); + signWrapper.lt("sign_day", DateUtil.format(new Date(), "yyyy-MM") + "-00"); + signWrapper.orderByAsc("create_time"); + List signRecordList = signRecordService.list(signWrapper); + //TaskCenter的number为大转盘次数 List taskCenters = baseMapper.queryTaskDiscCenter(userId); - //大转盘任务 校验 次数 - return 0; + int countTaskDisc = 0; + Integer dayOrderNum = ordersService.countOrderNum(userId, DateUtil.today() + " 00:00:00"); + for (TaskCenter taskCenter : taskCenters) { + if (taskCenter.getType().equals(2)) { + if (taskCenter.getNumber().equals(1)) { + if (dayOrderNum > 2 && recordService.countTaskNum(userId, taskCenter.getId(), DateUtil.today() + " 00:00:00") < 1) { + countTaskDisc = countTaskDisc + taskCenter.getDiscNumber(); + } + } else { + if (dayOrderNum > 2) { + if (signRecordList.size() - taskCenter.getNumber().intValue() >= -1 && recordService.countTaskNum(userId, taskCenter.getId(), DateUtil.beginOfMonth(new Date()).toString()) < 1) { + countTaskDisc = countTaskDisc + taskCenter.getDiscNumber(); + } + } else { + if (signRecordList.size() - taskCenter.getNumber().intValue() >= 0 && recordService.countTaskNum(userId, taskCenter.getId(), DateUtil.beginOfMonth(new Date()).toString()) < 1) { + countTaskDisc = countTaskDisc + taskCenter.getDiscNumber(); + } + } + + } + } + } + return countTaskDisc; } } diff --git a/src/main/resources/mapper/tashCenter/TaskCenterDto.xml b/src/main/resources/mapper/tashCenter/TaskCenterDto.xml index cdb6e2ce..d7682b5c 100644 --- a/src/main/resources/mapper/tashCenter/TaskCenterDto.xml +++ b/src/main/resources/mapper/tashCenter/TaskCenterDto.xml @@ -1,16 +1,15 @@ - - + + - - SELECT task.*, - reward.number as number + reward.number as discNumber FROM task_center_reward reward INNER JOIN task_center task ON reward.task_id = task.id and task.shows = 1 where reward.type = 9 and user_id = #{userId} - + \ No newline at end of file