This commit is contained in:
parent
9696e6e9de
commit
318d252a32
|
|
@ -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 "";
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import com.sqx.common.utils.DateUtils;
|
||||||
import com.sqx.common.utils.HttpContextUtils;
|
import com.sqx.common.utils.HttpContextUtils;
|
||||||
import com.sqx.common.utils.IPUtils;
|
import com.sqx.common.utils.IPUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import org.aspectj.lang.annotation.Around;
|
import org.aspectj.lang.annotation.Around;
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
|
@ -32,7 +33,6 @@ public class AppApiMethodAspect {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Around("pkg()")
|
@Around("pkg()")
|
||||||
// @SuppressWarnings("unchecked")
|
|
||||||
public Object around(ProceedingJoinPoint pjp) throws Throwable {
|
public Object around(ProceedingJoinPoint pjp) throws Throwable {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
// 执行被拦截的方法
|
// 执行被拦截的方法
|
||||||
|
|
@ -41,15 +41,16 @@ public class AppApiMethodAspect {
|
||||||
Object[] args = pjp.getArgs();
|
Object[] args = pjp.getArgs();
|
||||||
String params = new Gson().toJson(args);
|
String params = new Gson().toJson(args);
|
||||||
String resultJson = new Gson().toJson(result);
|
String resultJson = new Gson().toJson(result);
|
||||||
//获取request
|
|
||||||
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
|
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
|
||||||
long end = System.currentTimeMillis();
|
long end = System.currentTimeMillis();
|
||||||
|
if(StringUtils.isNotBlank(resultJson) && !"null".equals(resultJson)){
|
||||||
log.info("\n>>>>>> {} {}\n>>>>>> IP: {} \n>>>>>> execute time:{}\n>>>>>> Request: {}\n>>>>>> Response: {}",
|
log.info("\n>>>>>> {} {}\n>>>>>> IP: {} \n>>>>>> execute time:{}ms \n>>>>>> Request: {}\n>>>>>> Response: {}",
|
||||||
request.getMethod(), request.getRequestURL(), IPUtils.getIpAddr(request),end-start,
|
request.getMethod(), request.getRequestURL(), IPUtils.getIpAddr(request),end-start,
|
||||||
params,
|
params,
|
||||||
resultJson
|
resultJson
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<String, ConcurrentHashMap<Object, Long>> 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<Object, Long> 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<Object, Long> 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<String, ConcurrentHashMap<Object, Long>> outerEntry : executionTimeMap.entrySet()) {
|
||||||
|
String methodSignature = outerEntry.getKey();
|
||||||
|
ConcurrentHashMap<Object, Long> innerMap = outerEntry.getValue();
|
||||||
|
ConcurrentHashMap<Object, Long> keysToRemove = new ConcurrentHashMap<>();
|
||||||
|
for (Entry<Object, Long> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.google.common.util.concurrent.RateLimiter;
|
import com.google.common.util.concurrent.RateLimiter;
|
||||||
|
import com.sqx.common.annotation.Debounce;
|
||||||
import com.sqx.common.utils.DateUtils;
|
import com.sqx.common.utils.DateUtils;
|
||||||
import com.sqx.common.utils.Result;
|
import com.sqx.common.utils.Result;
|
||||||
import com.sqx.modules.app.annotation.Login;
|
import com.sqx.modules.app.annotation.Login;
|
||||||
|
|
@ -42,6 +43,7 @@ import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
|
|
@ -59,9 +61,6 @@ public class DiscSpinningController {
|
||||||
private final CashOutService cashOutService;
|
private final CashOutService cashOutService;
|
||||||
private final TaskCenterService taskCenterService;
|
private final TaskCenterService taskCenterService;
|
||||||
|
|
||||||
// 以id为键,对应的RateLimiter实例为值,用于不同id的防抖控制
|
|
||||||
private static final ConcurrentHashMap<Long, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DiscSpinningController(CommonInfoService commonRepository, DiscSpinningService discSpinningService,
|
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()));
|
draws(amount, orderId, userId, maps == null || maps.get("source") == null ? "order" : maps.get("source").toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/app/discSpinning/receive")
|
|
||||||
@ApiOperation("大转盘奖项领取")
|
@ApiOperation("大转盘奖项领取")
|
||||||
|
@Debounce(interval = 3000, value = "#receive.id")
|
||||||
|
@PostMapping("/app/discSpinning/receive")
|
||||||
public Result receive(@RequestBody DiscSpinningRecord receive) {
|
public Result receive(@RequestBody DiscSpinningRecord receive) {
|
||||||
// 每秒允许0.6次操作
|
DiscSpinningRecord record = recordService.getById(receive.getId());
|
||||||
RateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(receive.getId(), k -> RateLimiter.create(0.6));
|
CompletableFuture.runAsync(() -> {
|
||||||
if (rateLimiter.tryAcquire()) {
|
receiveAsync(record);
|
||||||
CompletableFuture.runAsync(() -> {
|
});
|
||||||
DiscSpinningRecord record = recordService.getById(receive.getId());
|
return Result.success();
|
||||||
CompletableFuture.runAsync(() -> {
|
|
||||||
receiveAsync(record);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return Result.success();
|
|
||||||
}
|
|
||||||
return Result.error("操作过于频繁,请稍后再试");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void receiveAsync(DiscSpinningRecord receive) {
|
public void receiveAsync(DiscSpinningRecord receive) {
|
||||||
UserEntity userInfo = userService.queryByUserId(receive.getUserId());
|
UserEntity userInfo = userService.queryByUserId(receive.getUserId());
|
||||||
UserMoneyDetails userMoneyDetails = new UserMoneyDetails(
|
UserMoneyDetails userMoneyDetails = new UserMoneyDetails(
|
||||||
receive.getUserId(),null,null,"[现金大转盘]",5,1,2,
|
receive.getUserId(), null, null, "[现金大转盘]", 5, 1, 2,
|
||||||
receive.getNumber(),"现金红包奖励" + receive.getNumber() + "元");
|
receive.getNumber(), "现金红包奖励" + receive.getNumber() + "元");
|
||||||
userMoneyDetailsService.save(userMoneyDetails);
|
userMoneyDetailsService.save(userMoneyDetails);
|
||||||
receive.setTarget("2");
|
receive.setTarget("2");
|
||||||
receive.setTargetId(userMoneyDetails.getId());
|
receive.setTargetId(userMoneyDetails.getId());
|
||||||
|
|
@ -247,8 +240,8 @@ public class DiscSpinningController {
|
||||||
cashOut.setCreateAt(DateUtil.now());
|
cashOut.setCreateAt(DateUtil.now());
|
||||||
|
|
||||||
UserMoneyDetails userMoneyDetails = new UserMoneyDetails(
|
UserMoneyDetails userMoneyDetails = new UserMoneyDetails(
|
||||||
userInfo.getUserId(),null,null,"[现金大转盘]",4,2,1,
|
userInfo.getUserId(), null, null, "[现金大转盘]", 4, 2, 1,
|
||||||
new BigDecimal(money),"现金红包自动提现" + money + "元");
|
new BigDecimal(money), "现金红包自动提现" + money + "元");
|
||||||
userMoneyDetailsService.save(userMoneyDetails);
|
userMoneyDetailsService.save(userMoneyDetails);
|
||||||
//减去余额 钱
|
//减去余额 钱
|
||||||
userMoneyService.updateAmount(2, userInfo.getUserId(), money);
|
userMoneyService.updateAmount(2, userInfo.getUserId(), money);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.sqx.modules.pay.controller.app;
|
package com.sqx.modules.pay.controller.app;
|
||||||
|
|
||||||
|
|
||||||
|
import com.sqx.common.annotation.Debounce;
|
||||||
import com.sqx.common.utils.PageUtils;
|
import com.sqx.common.utils.PageUtils;
|
||||||
import com.sqx.common.utils.Result;
|
import com.sqx.common.utils.Result;
|
||||||
import com.sqx.modules.app.annotation.Login;
|
import com.sqx.modules.app.annotation.Login;
|
||||||
|
|
@ -44,6 +45,7 @@ public class AppCashController {
|
||||||
|
|
||||||
@Login
|
@Login
|
||||||
@GetMapping(value = "/withdraw")
|
@GetMapping(value = "/withdraw")
|
||||||
|
@Debounce(interval = 3000, value = "#userId")
|
||||||
@ApiOperation("发起提现 余额 金钱")
|
@ApiOperation("发起提现 余额 金钱")
|
||||||
public Result withdraw(@RequestAttribute("userId") Long userId, Double amount) {
|
public Result withdraw(@RequestAttribute("userId") Long userId, Double amount) {
|
||||||
return cashOutService.withdraw(userId, amount, false);
|
return cashOutService.withdraw(userId, amount, false);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.sqx.modules.taskCenter.controller;
|
||||||
|
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import com.sqx.common.annotation.Debounce;
|
||||||
import com.sqx.modules.app.annotation.Login;
|
import com.sqx.modules.app.annotation.Login;
|
||||||
import com.sqx.modules.taskCenter.entity.TaskCenter;
|
import com.sqx.modules.taskCenter.entity.TaskCenter;
|
||||||
import com.sqx.modules.taskCenter.service.TaskCenterService;
|
import com.sqx.modules.taskCenter.service.TaskCenterService;
|
||||||
|
|
@ -80,6 +81,7 @@ public class TaskCenterController {
|
||||||
@ApiImplicitParam(name = "id", value = "任务id", dataTypeClass = Long.class),
|
@ApiImplicitParam(name = "id", value = "任务id", dataTypeClass = Long.class),
|
||||||
})
|
})
|
||||||
@ApiOperation("App 任务中心 领取")
|
@ApiOperation("App 任务中心 领取")
|
||||||
|
@Debounce(interval = 1000, value = "#userId")
|
||||||
public Result taskReceive(@ApiIgnore @RequestAttribute("userId") Long userId,Long id) {
|
public Result taskReceive(@ApiIgnore @RequestAttribute("userId") Long userId,Long id) {
|
||||||
return taskCenterService.taskReceive(userId,id);
|
return taskCenterService.taskReceive(userId,id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue