|
|
@@ -0,0 +1,169 @@
|
|
|
+package shop.alien.storeplatform.aspect;
|
|
|
+
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.aspectj.lang.ProceedingJoinPoint;
|
|
|
+import org.aspectj.lang.annotation.Around;
|
|
|
+import org.aspectj.lang.annotation.Aspect;
|
|
|
+import org.aspectj.lang.reflect.MethodSignature;
|
|
|
+import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.context.request.RequestContextHolder;
|
|
|
+import org.springframework.web.context.request.ServletRequestAttributes;
|
|
|
+import shop.alien.entity.result.R;
|
|
|
+import shop.alien.storeplatform.annotation.PreventDuplicateSubmit;
|
|
|
+import shop.alien.storeplatform.util.LoginUserUtil;
|
|
|
+
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 防重复提交切面
|
|
|
+ *
|
|
|
+ * @author ssk
|
|
|
+ * @since 2025-11-25
|
|
|
+ */
|
|
|
+@Aspect
|
|
|
+@Component
|
|
|
+@Slf4j
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class PreventDuplicateSubmitAspect {
|
|
|
+
|
|
|
+ private final StringRedisTemplate stringRedisTemplate;
|
|
|
+
|
|
|
+ @Around("@annotation(shop.alien.storeplatform.annotation.PreventDuplicateSubmit)")
|
|
|
+ public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
|
+ // 1. 获取注解信息
|
|
|
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
|
|
+ Method method = signature.getMethod();
|
|
|
+ PreventDuplicateSubmit annotation = method.getAnnotation(PreventDuplicateSubmit.class);
|
|
|
+
|
|
|
+ // 2. 构建Redis锁的key
|
|
|
+ String lockKey = buildLockKey(joinPoint, annotation);
|
|
|
+
|
|
|
+ log.info("PreventDuplicateSubmitAspect - 尝试获取锁: key={}", lockKey);
|
|
|
+
|
|
|
+ // 3. 尝试获取锁
|
|
|
+ Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
|
|
|
+ lockKey,
|
|
|
+ String.valueOf(System.currentTimeMillis()),
|
|
|
+ annotation.expireSeconds(),
|
|
|
+ TimeUnit.SECONDS
|
|
|
+ );
|
|
|
+
|
|
|
+ // 4. 获取锁失败,说明重复提交
|
|
|
+ if (success == null || !success) {
|
|
|
+ log.warn("PreventDuplicateSubmitAspect - 重复提交被拦截: key={}", lockKey);
|
|
|
+ return R.fail(annotation.message());
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 5. 执行业务方法
|
|
|
+ log.info("PreventDuplicateSubmitAspect - 获取锁成功,执行业务: key={}", lockKey);
|
|
|
+ Object result = joinPoint.proceed();
|
|
|
+
|
|
|
+ // 6. 如果业务执行失败,立即释放锁,允许重试
|
|
|
+ if (result instanceof R) {
|
|
|
+ R<?> r = (R<?>) result;
|
|
|
+ if (!r.isSuccess()) {
|
|
|
+ stringRedisTemplate.delete(lockKey);
|
|
|
+ log.info("PreventDuplicateSubmitAspect - 业务执行失败,释放锁: key={}", lockKey);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 7. 发生异常,释放锁
|
|
|
+ stringRedisTemplate.delete(lockKey);
|
|
|
+ log.error("PreventDuplicateSubmitAspect - 执行异常,释放锁: key={}", lockKey, e);
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建锁的key
|
|
|
+ * 格式: prefix:userId:methodName:参数摘要
|
|
|
+ */
|
|
|
+ private String buildLockKey(ProceedingJoinPoint joinPoint, PreventDuplicateSubmit annotation) {
|
|
|
+ // 获取当前用户ID
|
|
|
+ Integer userId = null;
|
|
|
+ try {
|
|
|
+ userId = LoginUserUtil.getCurrentUserId();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("PreventDuplicateSubmitAspect - 无法获取用户ID,使用IP作为标识");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取方法名
|
|
|
+ String methodName = joinPoint.getSignature().getName();
|
|
|
+
|
|
|
+ // 获取请求IP(作为备用标识)
|
|
|
+ String ip = getRequestIP();
|
|
|
+
|
|
|
+ // 获取参数摘要(只取金额等关键参数)
|
|
|
+ String paramKey = buildParamKey(joinPoint.getArgs());
|
|
|
+
|
|
|
+ // 构建key: prevent_duplicate:cashOut:userId_123:10000
|
|
|
+ return String.format("%s:%s:%s:%s",
|
|
|
+ annotation.keyPrefix(),
|
|
|
+ methodName,
|
|
|
+ userId != null ? "user_" + userId : "ip_" + ip,
|
|
|
+ paramKey
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建参数key(针对提现金额等关键参数)
|
|
|
+ */
|
|
|
+ private String buildParamKey(Object[] args) {
|
|
|
+ if (args == null || args.length == 0) {
|
|
|
+ return "noargs";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 针对提现接口,提取金额参数
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (Object arg : args) {
|
|
|
+ if (arg == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是CashOutDTO,提取金额
|
|
|
+ if (arg.getClass().getSimpleName().contains("CashOutDTO")) {
|
|
|
+ try {
|
|
|
+ Method getWithdrawalMoney = arg.getClass().getMethod("getWithdrawalMoney");
|
|
|
+ Object money = getWithdrawalMoney.invoke(arg);
|
|
|
+ sb.append(money);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("PreventDuplicateSubmitAspect - 无法提取提现金额");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return sb.length() > 0 ? sb.toString() : "default";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取请求IP
|
|
|
+ */
|
|
|
+ private String getRequestIP() {
|
|
|
+ try {
|
|
|
+ ServletRequestAttributes attributes =
|
|
|
+ (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
|
+ if (attributes != null) {
|
|
|
+ HttpServletRequest request = attributes.getRequest();
|
|
|
+ String ip = request.getHeader("X-Forwarded-For");
|
|
|
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
|
|
+ ip = request.getHeader("X-Real-IP");
|
|
|
+ }
|
|
|
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
|
|
+ ip = request.getRemoteAddr();
|
|
|
+ }
|
|
|
+ return ip != null ? ip.replaceAll("[^0-9.]", "") : "unknown";
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("PreventDuplicateSubmitAspect - 获取IP失败", e);
|
|
|
+ }
|
|
|
+ return "unknown";
|
|
|
+ }
|
|
|
+}
|
|
|
+
|