diff --git a/tyr-api/src/main/java/cn/axzo/tyr/client/common/annotation/RepeatedSubmit.java b/tyr-api/src/main/java/cn/axzo/tyr/client/common/annotation/RepeatedSubmit.java new file mode 100644 index 00000000..8e6c2ba9 --- /dev/null +++ b/tyr-api/src/main/java/cn/axzo/tyr/client/common/annotation/RepeatedSubmit.java @@ -0,0 +1,37 @@ +package cn.axzo.tyr.client.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 重复提交 + **/ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatedSubmit { + + /** + * 是否加锁,同时只有一人可提交数据.否则每个人同时只能提交一次 + * + * @return + */ + boolean unique() default false; + + /** + * 支持spEl表达式,自定义key,如果存在自定义key,刚使用自定义key + * unique则无效 + * + * @return + */ + String spElKey() default ""; + + /** + * 重复请求报错信息 + * 默认: 请勿重复提交 + */ + String msg() default "请勿重复提交"; +} diff --git a/tyr-api/src/main/java/cn/axzo/tyr/client/feign/DataObjectApi.java b/tyr-api/src/main/java/cn/axzo/tyr/client/feign/DataObjectApi.java index 2a68498b..fcf9572f 100644 --- a/tyr-api/src/main/java/cn/axzo/tyr/client/feign/DataObjectApi.java +++ b/tyr-api/src/main/java/cn/axzo/tyr/client/feign/DataObjectApi.java @@ -2,6 +2,7 @@ package cn.axzo.tyr.client.feign; import cn.axzo.basics.common.page.PageResult; import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.tyr.client.common.annotation.RepeatedSubmit; import cn.axzo.tyr.client.model.req.CreateDataObjectReq; import cn.axzo.tyr.client.model.req.DeleteDataObjectReq; import cn.axzo.tyr.client.model.req.EditDataObjectReq; diff --git a/tyr-server/src/main/java/cn/axzo/tyr/server/controller/data/object/DataObjectController.java b/tyr-server/src/main/java/cn/axzo/tyr/server/controller/data/object/DataObjectController.java index 88fb1bbb..e8366555 100644 --- a/tyr-server/src/main/java/cn/axzo/tyr/server/controller/data/object/DataObjectController.java +++ b/tyr-server/src/main/java/cn/axzo/tyr/server/controller/data/object/DataObjectController.java @@ -4,6 +4,7 @@ import cn.axzo.basics.auth.enums.WorkspaceTypeWithLegacyEnum; import cn.axzo.basics.common.page.PageResult; import cn.axzo.framework.domain.ServiceException; import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.tyr.client.common.annotation.RepeatedSubmit; import cn.axzo.tyr.client.common.enums.AttrPermissionEnum; import cn.axzo.tyr.client.common.enums.EnumTypeEnum; import cn.axzo.tyr.client.common.enums.ReturnCodeEnum; @@ -36,7 +37,7 @@ import java.util.stream.Collectors; public class DataObjectController implements DataObjectApi { private final DataObjectService dataObjectService; - + @RepeatedSubmit(spElKey = "'createDataObject:' + #req.dataObjectCode") public ApiResult createDataObject(CreateDataObjectReq req) { return ApiResult.ok(dataObjectService.createDataObject(req)); } diff --git a/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/DataObjectServiceImpl.java b/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/DataObjectServiceImpl.java index f3865ece..0f86c282 100644 --- a/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/DataObjectServiceImpl.java +++ b/tyr-server/src/main/java/cn/axzo/tyr/server/service/impl/DataObjectServiceImpl.java @@ -66,10 +66,10 @@ public class DataObjectServiceImpl implements DataObjectService { // 校验 // objectName、objectCode不能重复 checkObjectNameOrCodeUnique(req.getDataObjectCode(), req.getDataObjectName()); -// // 对象属性名和code不能重复 -// checkObjectAttrNameOrCodeUnique(req); -// // 规则名 不能重复 -// checkRuleNameUnique(req); + // 对象属性名和code不能重复 + checkObjectAttrNameOrCodeUnique(req); + // 规则名 不能重复 + checkRuleNameUnique(req); // 准备 DataObject dataObject = DataObjectMapper.INSTANCE.createReq2DataObject(req); diff --git a/tyr-server/src/main/java/cn/axzo/tyr/server/utils/RepeatedSubmitAspect.java b/tyr-server/src/main/java/cn/axzo/tyr/server/utils/RepeatedSubmitAspect.java new file mode 100644 index 00000000..85b1d820 --- /dev/null +++ b/tyr-server/src/main/java/cn/axzo/tyr/server/utils/RepeatedSubmitAspect.java @@ -0,0 +1,183 @@ +package cn.axzo.tyr.server.utils; + +import cn.axzo.framework.auth.domain.ContextInfo; +import cn.axzo.framework.auth.domain.ContextInfoHolder; +import cn.axzo.framework.domain.ServiceException; +import cn.axzo.tyr.client.common.annotation.RepeatedSubmit; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.StrUtil; +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.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +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 org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; +import java.util.StringJoiner; +import java.util.concurrent.TimeUnit; + +/** + * 重复提交切面 + * + * @author yangzhi + * @Date 2021/5/20 15:00 + **/ +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class RepeatedSubmitAspect { + + private final RedissonClient redissonClient; + + @Around("@annotation(repeatedSubmit)") + public Object lock(ProceedingJoinPoint joinPoint, RepeatedSubmit repeatedSubmit) + throws Exception { + String redisKey; + RLock lock = null; + try { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + HttpServletRequest httpServletRequest = ((ServletRequestAttributes) attributes).getRequest(); + String uri = httpServletRequest.getRequestURI(); + String methodName = httpServletRequest.getMethod(); + + String errorMsg = repeatedSubmit.msg(); + + StringJoiner stringJoiner = new StringJoiner(":"); + stringJoiner.add("URI_LIMIT"); + String spElKey = repeatedSubmit.spElKey(); + /* + * 1.如果指定了key,将使用自定义的key + * 2.否则使用uri当作redis的key + */ + if (CharSequenceUtil.isNotBlank(spElKey)) { + getKeyBySpElExpression(stringJoiner, joinPoint, repeatedSubmit); + } else { + stringJoiner.add(uri).add(methodName); + /* + * 1.unique为true时,redis不添加用户id + * 2.unique为false时,只针对当前用户做过滤 + */ + if (!repeatedSubmit.unique()) { + ContextInfo contextInfo = ContextInfoHolder.get(); + Long userId = null; + if (null != contextInfo) { + userId = contextInfo.getUserInfo().getPersonId(); + } + if (null == userId) { + throw new ServiceException("获取用户信息失败"); + } + stringJoiner.add(String.valueOf(userId)); + } + } + redisKey = stringJoiner.toString(); + /* + * redis加锁 + */ + lock = redissonClient.getLock(redisKey); + boolean flag = lock.tryLock(1500, TimeUnit.MILLISECONDS); + if (!flag) { + /*防止释放别人的锁,所以将其置空*/ + lock = null; + log.info("请稍后重试!,redisKey={}" + redisKey); + throw new ServiceException(errorMsg); + } + return joinPoint.proceed(); + } catch (Throwable throwable) { + throw (Exception) throwable; + } finally { + if (null != lock) { + try { + //释放锁时,有可能释放别人的锁,所以捕获异常,不抛异常 + lock.unlock(); + } catch (Exception e) { + } + } + } + } + + /** + * 根据spEl获取key值 + * + * @param stringJoiner + * @param joinPoint + * @param repeatedSubmit + */ + private void getKeyBySpElExpression(StringJoiner stringJoiner, ProceedingJoinPoint joinPoint + , RepeatedSubmit repeatedSubmit) { + //读取spEl表达式中的值 + Object[] args = joinPoint.getArgs(); + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + String spElValue = getSpElValue(args, method, repeatedSubmit.spElKey()); + stringJoiner.add(spElValue); + } + + /** + * 获取spEl值 + * + * @param arguments + * @param signatureMethod + * @param spelKey + * @return 返回spel值 + */ + private String getSpElValue(Object[] arguments, Method signatureMethod, String spelKey) { + EvaluationContext context = getContext(arguments, signatureMethod); + if (null == context) { + return CharSequenceUtil.EMPTY; + } + String value = getValue(context, spelKey, String.class); + if (CharSequenceUtil.isBlank(value)) { + return CharSequenceUtil.EMPTY; + } + return value; + } + + /** + * 获取spel 定义的参数值 + * + * @param context 参数容器 + * @param key key + * @param clazz 需要返回的类型 + * @param 返回泛型 + * @return 参数值 + */ + private T getValue(EvaluationContext context, String key, Class clazz) { + SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); + Expression expression = spelExpressionParser.parseExpression(key); + return expression.getValue(context, clazz); + } + + + /** + * 获取参数容器 + * + * @param arguments 方法的参数列表 + * @param signatureMethod 被执行的方法体 + * @return 装载参数的容器 + */ + private EvaluationContext getContext(Object[] arguments, Method signatureMethod) { + String[] parameterNames = new LocalVariableTableParameterNameDiscoverer() + .getParameterNames(signatureMethod); + if (parameterNames == null) { + return null; + } + EvaluationContext context = new StandardEvaluationContext(); + for (int i = 0; i < arguments.length; i++) { + context.setVariable(parameterNames[i], arguments[i]); + } + return context; + } +}