This commit is contained in:
wangw 2025-03-11 10:24:14 +08:00
parent 7f06b4d1c4
commit ea7fea9f48
10 changed files with 222 additions and 44 deletions

View File

@ -2,6 +2,7 @@ package com.czg.controller;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import com.czg.annotation.Debounce;
import com.czg.order.dto.CheckOrderPay;
import com.czg.order.entity.OrderInfo;
import com.czg.order.service.OrderInfoService;
@ -39,12 +40,14 @@ public class OrderPayController {
private SysParamsService paramsService;
@PostMapping("/creditPay")
@Debounce(value = "#payParam.checkOrderPay.orderId")
public CzgResult<Object> creditPayOrder(@RequestHeader Long shopId, @Validated @RequestBody OrderPayParamDTO payParam) {
payParam.setShopId(shopId);
return payService.creditPayOrder(payParam);
}
@PostMapping("/cashPay")
@Debounce(value = "#payParam.checkOrderPay.orderId")
public CzgResult<Object> cashPayOrder(@RequestHeader Long shopId, @Validated @RequestBody OrderPayParamDTO payParam) {
payParam.setShopId(shopId);
return payService.cashPayOrder(payParam);
@ -60,6 +63,7 @@ public class OrderPayController {
* accountPay(小程序使用) 密码支付 用户密码pwd 必填
*/
@PostMapping("/vipPay")
@Debounce(value = "#payParam.checkOrderPay.orderId")
public CzgResult<Object> vipPayOrder(@RequestHeader Long shopId, @Validated @RequestBody OrderPayParamDTO payParam) {
payParam.setShopId(shopId);
AssertUtil.isBlank(payParam.getPayType(), "支付类型不可为空");
@ -70,9 +74,10 @@ public class OrderPayController {
* h5支付
*/
@PostMapping("/h5Pay")
@Debounce(value = "#payParam.checkOrderPay.orderId")
public CzgResult<Map<String, Object>> h5PayOrder(@RequestHeader Long shopId, HttpServletRequest request, @Validated @RequestBody OrderPayParamDTO payParam) {
payParam.setShopId(shopId);
return payService.h5PayOrder(ServletUtil.getClientIP(request,""), payParam);
return payService.h5PayOrder(ServletUtil.getClientIP(request, ""), payParam);
}
/**
@ -82,6 +87,7 @@ public class OrderPayController {
* openId 必填
*/
@PostMapping("/jsPay")
@Debounce(value = "#payParam.checkOrderPay.orderId")
public CzgResult<Map<String, Object>> jsPayOrder(@RequestHeader Long shopId, HttpServletRequest request, @Validated @RequestBody OrderPayParamDTO payParam) {
payParam.setShopId(shopId);
return payService.jsPayOrder(ServletUtil.getClientIP(request), payParam);
@ -93,6 +99,7 @@ public class OrderPayController {
* openId 必填
*/
@PostMapping("/ltPayOrder")
@Debounce(value = "#payParam.checkOrderPay.orderId")
public CzgResult<Map<String, Object>> ltPayOrder(@RequestHeader Long shopId, HttpServletRequest request, @Validated @RequestBody OrderPayParamDTO payParam) {
payParam.setShopId(shopId);
return payService.ltPayOrder(ServletUtil.getClientIP(request), payParam);
@ -102,6 +109,7 @@ public class OrderPayController {
* 正扫
*/
@PostMapping("/scanPay")
@Debounce(value = "#payParam.checkOrderPay.orderId")
public CzgResult<Map<String, Object>> scanPayOrder(@RequestHeader Long shopId, HttpServletRequest request, @Validated @RequestBody OrderPayParamDTO payParam) {
payParam.setShopId(shopId);
return payService.scanPayOrder(ServletUtil.getClientIP(request), payParam);
@ -112,6 +120,7 @@ public class OrderPayController {
* authCode 必填 扫描码
*/
@PostMapping("/microPay")
@Debounce(value = "#payParam.checkOrderPay.orderId")
public CzgResult<Map<String, Object>> microPayOrder(@RequestHeader Long shopId, @Validated @RequestBody OrderPayParamDTO payParam) {
payParam.setShopId(shopId);
return payService.microPayOrder(payParam);
@ -121,6 +130,7 @@ public class OrderPayController {
* 获取店铺订单支付URL
*/
@GetMapping("/shopPayApi/orderPayUrl")
@Debounce(value = "#checkOrderPay.orderId")
public CzgResult<String> getOrderPayUrl(@RequestHeader Long shopId, @RequestParam(required = false) String extend,
CheckOrderPay checkOrderPay) {
AssertUtil.isNull(shopId, "店铺id不能为空");
@ -146,6 +156,7 @@ public class OrderPayController {
* checkOrderPay.orderAmount 必填
*/
@PostMapping("/shopPayApi/js2Pay")
@Debounce(value = "#payParam.checkOrderPay.orderId")
public CzgResult<Map<String, Object>> js2PayOrder(HttpServletRequest request, @RequestBody OrderPayParamDTO payParam) {
return payService.js2PayOrder(ServletUtil.getClientIP(request), payParam);
}

View File

@ -1,5 +1,6 @@
package com.czg.controller;
import com.czg.annotation.Debounce;
import com.czg.annotation.SaStaffCheckPermission;
import com.czg.resp.CzgResult;
import com.czg.service.order.dto.VipPayParamDTO;
@ -36,6 +37,7 @@ public class VipPayController {
*/
@SaStaffCheckPermission("yun_xu_shou_kuan")
@PostMapping("/cashPayVip")
@Debounce(value = "#payParam.shopUserId")
public CzgResult<Object> cashPayVip(@Validated @RequestBody VipPayParamDTO payParam) {
AssertUtil.isNull(payParam.getShopUserId(), "充值失败 未指定店铺用户Id");
payParam.setPlatformType(ServletUtil.getHeaderIgnoreCase(ServletUtil.getRequest(), "platformType"));
@ -48,6 +50,7 @@ public class VipPayController {
* openId 必填
*/
@PostMapping("/jsPayVip")
@Debounce(value = "#payParam.shopUserId")
public CzgResult<Map<String, Object>> jsPayVip(HttpServletRequest request, @Validated @RequestBody VipPayParamDTO payParam) {
AssertUtil.isNull(payParam.getShopUserId(), "充值失败 未指定店铺用户Id");
payParam.setPlatformType(ServletUtil.getHeaderIgnoreCase(ServletUtil.getRequest(), "platformType"));
@ -60,6 +63,7 @@ public class VipPayController {
* openId 必填
*/
@PostMapping("/ltPayVip")
@Debounce(value = "#payParam.shopUserId")
public CzgResult<Map<String, Object>> ltPayVip(HttpServletRequest request, @Validated @RequestBody VipPayParamDTO payParam) {
AssertUtil.isNull(payParam.getShopUserId(), "充值失败 未指定店铺用户Id");
payParam.setPlatformType(ServletUtil.getHeaderIgnoreCase(ServletUtil.getRequest(), "platformType"));
@ -71,6 +75,7 @@ public class VipPayController {
*/
@SaStaffCheckPermission("yun_xu_shou_kuan")
@PostMapping("/scanPayVip")
@Debounce(value = "#payParam.shopUserId")
public CzgResult<Map<String, Object>> scanPayVip(HttpServletRequest request, @Validated @RequestBody VipPayParamDTO payParam) {
AssertUtil.isNull(payParam.getShopUserId(), "充值失败 未指定店铺用户Id");
payParam.setPlatformType(ServletUtil.getHeaderIgnoreCase(request, "platformType"));
@ -83,6 +88,7 @@ public class VipPayController {
*/
@SaStaffCheckPermission("yun_xu_shou_kuan")
@PostMapping("/microPayVip")
@Debounce(value = "#payParam.shopUserId")
public CzgResult<Map<String, Object>> microPayVip(@Validated @RequestBody VipPayParamDTO payParam) {
AssertUtil.isNull(payParam.getShopUserId(), "充值失败 未指定店铺用户Id");
payParam.setPlatformType(ServletUtil.getHeaderIgnoreCase(ServletUtil.getRequest(), "platformType"));
@ -95,6 +101,7 @@ public class VipPayController {
*/
@SaStaffCheckPermission("yun_xu_tui_kuan")
@PostMapping("/refundVipBefore")
@Debounce(value = "#payParam.flowId")
public CzgResult<Map<String, BigDecimal>> refundVipBefore(@Validated @RequestBody VipRefundDTO payParam) {
return payService.refundVipBefore(payParam);
}
@ -110,6 +117,7 @@ public class VipPayController {
*/
@SaStaffCheckPermission("yun_xu_tui_kuan")
@PostMapping("/refundVip")
@Debounce(value = "#payParam.flowId")
public CzgResult<Object> refundVip(HttpServletRequest request, @Validated @RequestBody VipRefundDTO payParam) {
AssertUtil.isNull(payParam.getRefAmount(), "退款金额不能为空");
if (payParam.getRefAmount().compareTo(BigDecimal.ZERO) <= 0) {

View File

@ -1,5 +1,6 @@
package com.czg.controller.admin;
import com.czg.annotation.Debounce;
import com.czg.annotation.SaStaffCheckPermission;
import com.czg.order.dto.OrderInfoAddDTO;
import com.czg.order.dto.OrderInfoPrintDTO;
@ -19,8 +20,6 @@ import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 订单管理
@ -65,6 +64,7 @@ public class AdminOrderController {
@SaStaffCheckPermission("yun_xu_xia_dan")
@PostMapping("/createOrder")
@Debounce(value = "#addDto.tableCode")
public CzgResult<OrderInfo> createOrder(@Validated @RequestBody OrderInfoAddDTO addDto) {
addDto.setPlatformType(ServletUtil.getHeaderIgnoreCase(ServletUtil.getRequest(), "platformType"));
addDto.setStaffId(StpKit.USER.getLoginIdAsLong());
@ -89,6 +89,7 @@ public class AdminOrderController {
*/
@SaStaffCheckPermission("yun_xu_tui_kuan")
@PostMapping("/refundOrder")
@Debounce(value = "#refundDTO.orderId")
public CzgResult<Object> refundOrder(@Validated @RequestBody OrderInfoRefundDTO refundDTO) {
return payService.refundOrderBefore(refundDTO);
}
@ -97,6 +98,7 @@ public class AdminOrderController {
* 订单打印
*/
@PostMapping("/print")
@Debounce(value = "#orderInfoPrintDTO.id")
public CzgResult<Boolean> printOrder(@Validated @RequestBody OrderInfoPrintDTO orderInfoPrintDTO) {
return CzgResult.success(orderInfoService.printOrder(StpKit.USER.getShopId(), orderInfoPrintDTO));
}

View File

@ -1,5 +1,6 @@
package com.czg.controller.user;
import com.czg.annotation.Debounce;
import com.czg.order.dto.OrderInfoAddDTO;
import com.czg.order.dto.OrderInfoQueryDTO;
import com.czg.order.entity.OrderInfo;
@ -15,8 +16,6 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 订单管理
@ -58,6 +57,7 @@ public class UserOrderController {
* 生成订单
*/
@PostMapping("/createOrder")
@Debounce(value = "#addDto.tableCode")
public CzgResult<OrderInfo> createOrder(@RequestBody OrderInfoAddDTO addDto) {
addDto.setPlatformType(ServletUtil.getHeaderIgnoreCase(ServletUtil.getRequest(), "platformType"));
long loginIdAsLong = StpKit.USER.getLoginIdAsLong();

View File

@ -0,0 +1,24 @@
package com.czg.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 "";
}

View File

@ -1,4 +1,4 @@
package com.czg.config;
package com.czg.aspect;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;

View File

@ -0,0 +1,118 @@
package com.czg.aspect;
import cn.hutool.core.util.StrUtil;
import com.czg.annotation.Debounce;
import com.czg.resp.CzgResult;
import com.czg.utils.SpELUtil;
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.czg.annotation.Debounce)")
public void debouncePointCut() {
}
// 用于存储基于方法和入参情况的上次执行时间结构为方法签名 -> (入参值 -> 上次执行时间)
private static final ConcurrentHashMap<String, ConcurrentHashMap<Object, Long>> EXECUTION_TIME_MAP = new ConcurrentHashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock();
@Around("debouncePointCut()")
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 (StrUtil.isBlank(value)) {
// 没有指定入参表达式按照整个方法进行防抖
return debounceForWholeMethod(joinPoint, methodSignature, interval, timeUnit);
}
String[] split = value.split(",");
StringBuilder values = new StringBuilder();
for (String str : split) {
values.append(SpELUtil.getByEl(str, joinPoint));
}
// 解析入参表达式获取对应入参的值
if (StrUtil.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 = EXECUTION_TIME_MAP.computeIfAbsent(methodSignature, k -> new ConcurrentHashMap<>());
long currentTime = System.currentTimeMillis();
Long lastTime = methodExecutionTimeMap.get(methodSignature);
if (lastTime == null) {
// 如果不存在对应键值对设置初始值这里设置为一个表示很久之前的时间戳比如0
lastTime = 0L;
}
if (currentTime - timeUnit.toMillis(interval) >= lastTime) {
// 满足防抖间隔更新上次执行时间并执行目标方法
methodExecutionTimeMap.put(methodSignature, currentTime);
return joinPoint.proceed();
}
// 在防抖间隔内不执行目标方法直接返回
return CzgResult.failure("请求频繁,请重试");
}
private Object debounceForSpecificValue(ProceedingJoinPoint joinPoint, String methodSignature, long interval, TimeUnit timeUnit, Object targetValue) throws Throwable {
ConcurrentHashMap<Object, Long> methodExecutionTimeMap = EXECUTION_TIME_MAP.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 CzgResult.failure("请求频繁,请重试");
}
public void cleanExpiredRecords() {
long expirationTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(10);
LOCK.lock();
try {
for (Entry<String, ConcurrentHashMap<Object, Long>> outerEntry : EXECUTION_TIME_MAP.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()) {
EXECUTION_TIME_MAP.remove(methodSignature);
}
}
} finally {
LOCK.unlock();
}
}
}

View File

@ -1,11 +0,0 @@
package com.czg;
/**
* @author tankaikai
* @since ${YEAR}-${MONTH}-${DAY} ${TIME}
*/
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}

View File

@ -1,27 +0,0 @@
package com.czg;
import com.czg.validator.group.DefaultGroup;
import com.czg.validator.group.InsertGroup;
import com.czg.validator.group.UpdateGroup;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author GYJoker
*/
@Data
@Accessors(chain = true)
public class SysParamsDTO2 {
@NotBlank(message = "参数编码不能为空", groups = {InsertGroup.class, UpdateGroup.class})
private String paramCode;
@NotBlank(message = "参数值不能为空", groups = DefaultGroup.class)
private String paramValue;
@NotNull(message = "参数类型不能为空", groups = {UpdateGroup.class, UpdateGroup.class})
private Integer paramType;
private String remark;
}

View File

@ -0,0 +1,53 @@
package com.czg.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;
import java.util.Objects;
/**
* @author ww
*/
public class SpELUtil {
/**
* 用于SpEL表达式解析.
*/
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
/**
* 用于获取方法参数定义名字.
*/
private static final DefaultParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
/**
* 解析SpEL表达式
*/
public static String getByEl(String spElStr, ProceedingJoinPoint joinPoint) {
// 通过joinPoint获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用Spring的DefaultParameterNameDiscoverer获取方法形参名数组
String[] paramNames = NAME_DISCOVERER.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++) {
assert paramNames != null;
context.setVariable(paramNames[i], args[i]);
}
if(expression.getValue(context)==null){
return "";
}
return Objects.requireNonNull(expression.getValue(context)).toString();
}
}