From 318d252a326f432452572991bb5e04d4097cf4d8 Mon Sep 17 00:00:00 2001 From: wangw <1594593906@qq.com> Date: Mon, 9 Dec 2024 14:51:10 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=B2=E6=8A=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sqx/common/annotation/Debounce.java | 24 ++++ .../sqx/common/aspect/AppApiMethodAspect.java | 17 +-- .../com/sqx/common/aspect/DebounceAspect.java | 117 ++++++++++++++++++ .../java/com/sqx/common/utils/SpelUtil.java | 52 ++++++++ .../controller/DiscSpinningController.java | 33 ++--- .../pay/controller/app/AppCashController.java | 2 + .../controller/TaskCenterController.java | 2 + 7 files changed, 219 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/sqx/common/annotation/Debounce.java create mode 100644 src/main/java/com/sqx/common/aspect/DebounceAspect.java create mode 100644 src/main/java/com/sqx/common/utils/SpelUtil.java 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..5151fe4a 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,7 +33,6 @@ public class AppApiMethodAspect { } @Around("pkg()") -// @SuppressWarnings("unchecked") public Object around(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); // 执行被拦截的方法 @@ -41,15 +41,16 @@ public class AppApiMethodAspect { Object[] args = pjp.getArgs(); String params = new Gson().toJson(args); 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/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/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); }