Browse Source

订单初始化

chao 3 năm trước cách đây
mục cha
commit
cbc32d7706

+ 29 - 21
.gitignore

@@ -1,25 +1,33 @@
-# ---> Java
-*.class
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
 
-# Package Files #
-*.jar
-*.war
-*.ear
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
 
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
 
-# ---> Maven
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
 
+### VS Code ###
+.vscode/

+ 198 - 0
pom.xml

@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.4.3</version>
+        <relativePath/>
+    </parent>
+    <groupId>com.caimei365.order</groupId>
+    <artifactId>caimei365-cloud-order</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>caimei365-cloud-order</name>
+    <description>采美365微服务-订单服务</description>
+    <properties>
+        <java.version>1.8</java.version>
+        <spring-cloud.version>2020.0.2</spring-cloud.version>
+    </properties>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring-cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-config</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-bootstrap</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+        <!-- mysql -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+            <version>1.2.5</version>
+            <exclusions>
+                <exclusion>
+                    <artifactId>mybatis-spring-boot-starter</artifactId>
+                    <groupId>org.mybatis.spring.boot</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- redis依赖包 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>io.lettuce</groupId>
+                    <artifactId>lettuce-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>redis.clients</groupId>
+            <artifactId>jedis</artifactId>
+        </dependency>
+        <!-- jwt -->
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.14.0</version>
+        </dependency>
+        <!-- aop -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <!-- fastjson -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.75</version>
+        </dependency>
+
+
+
+
+
+
+
+
+
+        <!-- knife4j:swagger增强-->
+        <!-- https://doc.xiaominfo.com/knife4j/documentation/get_start.html -->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+            <version>3.0.2</version>
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+            <version>3.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.projectreactor</groupId>
+            <artifactId>reactor-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>dev</id>
+            <properties>
+                <!-- 环境标识,需要与配置文件的名称相对应 -->
+                <activatedProperties>dev</activatedProperties>
+            </properties>
+            <activation>
+                <!-- 默认环境 -->
+                <activeByDefault>true</activeByDefault>
+            </activation>
+        </profile>
+        <profile>
+            <id>beta</id>
+            <properties>
+                <activatedProperties>beta</activatedProperties>
+            </properties>
+        </profile>
+        <profile>
+            <id>prod</id>
+            <properties>
+                <activatedProperties>prod</activatedProperties>
+            </properties>
+        </profile>
+    </profiles>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <!--可以在此配置过滤文件  -->
+                <includes>
+                    <include>**/*.yml</include>
+                    <include>**/*.xml</include>
+                </includes>
+                <!--开启filtering功能  -->
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 24 - 0
src/main/java/com/caimei365/order/OrderApplication.java

@@ -0,0 +1,24 @@
+package com.caimei365.order;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+/**
+ * `@EnableEurekaClient`: 声明一个Eureka客户端,只能注册到Eureka Server
+ * `@EnableDiscoveryClient`: 声明一个可以被发现的客户端,可以是其他注册中心
+ * `@EnableFeignClients`: 开启Feign
+ *
+ * @author : Charles
+ * @date : 2021/2/22
+ */
+@EnableDiscoveryClient
+@SpringBootApplication
+//@EnableFeignClients
+public class OrderApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(OrderApplication.class, args);
+    }
+
+}

+ 294 - 0
src/main/java/com/caimei365/order/components/RedisService.java

@@ -0,0 +1,294 @@
+package com.caimei365.order.components;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis 服务工具类
+ *
+ * @author : Charles
+ * @date : 2021/3/4
+ */
+@Slf4j
+@Service
+public class RedisService {
+
+	@Resource
+	private RedisTemplate<Serializable, Object> redisTemplate;
+
+    /**
+     * 批量删除
+     * @param keys
+     */
+	public void remove(String... keys) {
+		for(String key :keys){
+			remove(key);
+		}
+	}
+
+    /**
+     * 批量删除正则匹配到的
+     * @param pattern
+     */
+	public void removePattern(String pattern) {
+		Set<Serializable> keys = redisTemplate.keys(pattern);
+        assert keys != null;
+        if (keys.size() > 0){
+			redisTemplate.delete(keys);
+		}
+	}
+
+    /**
+     * 删除
+     * @param key
+     */
+	public void remove(String key) {
+		if (exists(key)) {
+			redisTemplate.delete(key);
+		}
+	}
+
+    /**
+     * 判断缓存中是否存在
+     * @param key
+     * @return boolean
+     */
+	public boolean exists(String key) {
+		return StringUtils.isBlank(key) ? false : redisTemplate.hasKey(key);
+	}
+
+    /**
+     * 读取缓存
+     * @param key
+     * @return
+     */
+	public Object get(String key) {
+		Object result = null;
+		ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
+		result = operations.get(key);
+		return result;
+	}
+
+    /**
+     * 写入缓存
+     * @param key
+     * @param value
+     * @return
+     */
+	public boolean set(String key, Object value) {
+		boolean result = false;
+		try {
+			ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
+			operations.set(key, value);
+			result = true;
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return result;
+	}
+
+    /**
+     * 写入缓存并加上过期时间(秒)
+     * @param key
+     * @param value
+     * @param expireTimeSeconds
+     * @return
+     */
+	public boolean set(String key, Object value, Long expireTimeSeconds) {
+		boolean result = false;
+		try {
+			ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
+			operations.set(key, value);
+			redisTemplate.expire(key, expireTimeSeconds, TimeUnit.SECONDS);
+			result = true;
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return result;
+	}
+    /**
+     * 写入过期时间(秒)
+     * @param key
+     * @param expireTimeSeconds
+     * @return
+     */
+	public boolean expire(String key, Long expireTimeSeconds) {
+		boolean result = false;
+		try {
+			redisTemplate.expire(key, expireTimeSeconds, TimeUnit.SECONDS);
+			result = true;
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return result;
+	}
+    /* **************************** 针对list操作的方法 **************************** */
+    /**
+     * 在key对应list的尾部添加
+     * @param key
+     * @param value
+     * @return
+     */
+	public long rightPushForList(String key, Object value) {
+		return redisTemplate.opsForList().rightPush(key, value);
+	}
+
+    /**
+     * 在key对应list的头部添加
+     * @param key
+     * @param value
+     * @return
+     */
+	public long leftPushForList(String key, Object value) {
+		return redisTemplate.opsForList().leftPush(key, value);
+	}
+
+    /**
+     * key对应list的长度
+     * @param key
+     * @return
+     */
+	public long listSize(String key) {
+		return redisTemplate.opsForList().size(key);
+	}
+
+    /**
+     * 获取list集合
+     * @param Key
+     * @param begin
+     * @param end
+     * @return
+     */
+	public List<?> getList(String Key, int begin, int end) {
+		return redisTemplate.opsForList().range(Key, begin, end);
+	}
+
+    /**
+     * 在key对应list的尾部移除
+     * @param key
+     * @return
+     */
+	public Object rightPopForList(String key) {
+		return redisTemplate.opsForList().rightPop(key);
+	}
+
+    /**
+     * 在key对应list的头部移除
+     * @param key
+     * @return
+     */
+	public Object leftPopForList(String key) {
+		return redisTemplate.opsForList().leftPop(key);
+	}
+
+    /**
+     * 移除list的index位置上的值
+     * @param Key
+     * @param index
+     * @param value
+     */
+	public void removeList(String Key, long index, Object value) {
+		redisTemplate.opsForList().remove(Key, index, value);
+	}
+
+    /**
+     * 重置list的index位置上的值
+     * @param Key
+     * @param index
+     * @param value
+     */
+	public void setList(String Key, long index, Object value) {
+		redisTemplate.opsForList().set(Key, index, value);
+	}
+
+
+    /**
+     * 写入list
+     * @param key
+     * @param list
+     */
+	public void setList(String key, List list) {
+		if(list!=null&&list.size()>0){
+			for (Object object : list) {
+				rightPushForList(key,object);
+			}
+		}
+	}
+
+    /**
+     * 写入map
+     * @param key
+     * @param map
+     */
+	public void setMap(String key, Map<String, Object> map) {
+		redisTemplate.opsForHash().putAll(key, map);
+	}
+
+    /**
+     * 获取map
+     * @param key
+     * @param mapKey
+     * @return
+     */
+	public Object get(String key, String mapKey) {
+		return redisTemplate.opsForHash().get(key, mapKey);
+	}
+
+    /**
+     * 写入map
+     * @param key
+     * @param hashKey
+     * @param hashValue
+     */
+	public void setMapByKV(String key, Object hashKey, Object hashValue) {
+		redisTemplate.opsForHash().put(key, hashKey,hashValue);
+	}
+
+    /**
+     * 删除map中的某个key-value
+     * @param key
+     * @param hashKey
+     */
+	public void removeHash(String key, String hashKey) {
+		redisTemplate.opsForHash().delete(key, hashKey);
+	}
+
+    /**
+     *
+     * @param key
+     * @return
+     */
+	public Map<Object, Object> getEntries(String key) {
+		return redisTemplate.opsForHash().entries(key);
+	}
+
+    /**
+     *
+     * @param key
+     * @param step
+     * @return
+     */
+	public long increase(String key, long step) {
+		return redisTemplate.opsForValue().increment(key, step);
+	}
+
+    /**
+     * 获取失效时间
+     * @param key
+     * @return
+     */
+	public long getExpireTime(String key) {
+		return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+	}
+
+}

+ 69 - 0
src/main/java/com/caimei365/order/config/RedisCacheConfig.java

@@ -0,0 +1,69 @@
+package com.caimei365.order.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+
+import java.io.Serializable;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2021/4/28
+ */
+@Configuration
+@EnableCaching
+public class RedisCacheConfig implements Serializable {
+    /**
+     * 最新版,设置redis缓存过期时间
+     */
+
+    @Bean
+    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
+        return new RedisCacheManager(
+           RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
+           // 默认策略,未配置的 key 会使用这个,3小时失效
+           this.getRedisCacheConfigurationWithTtl( 3 * 60 * 60),
+           // 指定 key 策略
+           this.getRedisCacheConfigurationMap()
+        );
+    }
+
+    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
+        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
+        //自定义设置缓存时间,6小时
+        // redisCacheConfigurationMap.put("getCommodityClassify", this.getRedisCacheConfigurationWithTtl(6 * 60 * 60));
+        return redisCacheConfigurationMap;
+    }
+
+
+    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
+        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
+        ObjectMapper om = new ObjectMapper();
+        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        jackson2JsonRedisSerializer.setObjectMapper(om);
+        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
+        redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
+                RedisSerializationContext
+                        .SerializationPair
+                        .fromSerializer(jackson2JsonRedisSerializer)
+        ).entryTtl(Duration.ofSeconds(seconds));
+
+        return redisCacheConfiguration;
+    }
+
+}

+ 123 - 0
src/main/java/com/caimei365/order/config/TokenFilter.java

@@ -0,0 +1,123 @@
+package com.caimei365.order.config;
+
+import com.alibaba.fastjson.JSONObject;
+import com.caimei365.order.components.RedisService;
+import com.caimei365.order.utils.JwtUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * JWT Token
+ *
+ * 续签逻辑:
+ * 登录成功后,用户在未过期时间内继续操作,续签token。
+ * 登录成功后,空闲超过过期时间,返回token已失效,重新登录。
+ * 实现逻辑:
+ * 1.登录成功后将token存储到redis里面(这时候k、v值一样都为token),并设置过期时间为token过期时间
+ * 2.当用户请求时token值还未过期,则重新设置redis里token的过期时间。
+ * 3.当用户请求时token值已过期,但redis中还在,则JWT重新生成token并覆盖v值(这时候k、v值不一样了),然后设置redis过期时间。
+ * 4.当用户请求时token值已过期,并且redis中也不存在,则用户空闲超时,返回token已失效,重新登录。
+ *
+ * @author : Charles
+ * @date : 2021/3/30
+ */
+@Slf4j
+@Component
+public class TokenFilter implements WebFilter {
+
+    private static final String AUTH = "X-Token";
+    /**
+     * 需要权限认证的接口路径
+     */
+    private static final String[] PERMISSION_URLS = new String[]{
+        "/commodity/shop/product/release",
+        "/commodity/shop/product/offline"
+    };
+    private RedisService redisService;
+    @Autowired
+    public void setRedisService(RedisService redisService) {
+        this.redisService = redisService;
+    }
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
+        ServerHttpRequest request = exchange.getRequest();
+        ServerHttpResponse response = exchange.getResponse();
+
+        HttpHeaders header = request.getHeaders();
+        String token = header.getFirst(AUTH);
+        String url = request.getURI().getPath();
+
+        if (StringUtils.isBlank(token) && Arrays.asList(PERMISSION_URLS).contains(url)) {
+            log.error("未经授权,Token为空!");
+            return tokenErrorResponse(response, "未经授权,Token为空!");
+        }
+
+        if (StringUtils.isNotBlank(token)) {
+            String cacheToken = (String) redisService.get(token);
+            // token续签
+            if (StringUtils.isNotBlank(cacheToken) && !"null".equals(cacheToken) && JwtUtil.isVerify(cacheToken)) {
+                int userId = JwtUtil.parseTokenUid(cacheToken);
+                log.debug("Token续签,UserId:"+userId+",Token:"+token);
+                // 再次校验token有效性
+                if (!JwtUtil.isVerify(cacheToken)) {
+                    // 生成token
+                    String newToken = JwtUtil.createToken(userId);
+                    // 将token存入redis,并设置超时时间(秒)
+                    redisService.set(token, newToken, JwtUtil.getExpireTime());
+                } else {
+                    // 重新设置超时时间(秒)
+                    redisService.expire(token, JwtUtil.getExpireTime());
+                }
+            } else {
+                // 需要验证的路径
+                if(Arrays.asList(PERMISSION_URLS).contains(url)) {
+                    // Token失效
+                    log.error("Token失效,token:"+token+",cacheToken:"+cacheToken);
+                    return tokenErrorResponse(response, "Token失效,请重新登录!");
+                }
+            }
+        }
+        //            //TODO 将用户信息存放在请求header中传递给下游业务
+        //            ServerHttpRequest.Builder mutate = request.mutate();
+        //            mutate.header("demo-user-name", username);
+        //            ServerHttpRequest buildReuqest = mutate.build();
+        //
+        //            //todo 如果响应中需要放数据,也可以放在response的header中
+        //            response.setStatusCode(HttpStatus.OK);
+        //            response.getHeaders().add("demo-user-name",username);
+        //            return chain.filter(exchange.mutate()
+        //                    .request(buildReuqest)
+        //                    .response(response)
+        //                    .build());
+        return chain.filter(exchange);
+    }
+
+    private Mono<Void> tokenErrorResponse(ServerHttpResponse response, String responseMsg){
+        response.setStatusCode(HttpStatus.OK);
+        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
+        JSONObject res = new JSONObject();
+        res.put("code", -99);
+        res.put("msg", responseMsg);
+        byte[] responseByte = res.toJSONString().getBytes(StandardCharsets.UTF_8);
+        DataBuffer buffer = response.bufferFactory().wrap(responseByte);
+        return response.writeWith(Flux.just(buffer));
+    }
+
+
+}

+ 27 - 0
src/main/java/com/caimei365/order/idempotent/Idempotent.java

@@ -0,0 +1,27 @@
+package com.caimei365.order.idempotent;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义幂等注解
+ *
+ * @author : Charles
+ * @date : 2021/2/26
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Idempotent {
+    /**
+     * 前缀属性,作为redis缓存Key的一部分。
+     */
+    String prefix() default "idempotent_";
+    /**
+     * 需要的参数名数组
+     */
+    String[] keys();
+    /**
+     * 幂等过期时间(秒),即:在此时间段内,对API进行幂等处理。
+     */
+    int expire() default 3;
+}

+ 104 - 0
src/main/java/com/caimei365/order/idempotent/IdempotentAspect.java

@@ -0,0 +1,104 @@
+package com.caimei365.order.idempotent;
+
+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.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+import redis.clients.jedis.commands.JedisCommands;
+import redis.clients.jedis.params.SetParams;
+
+import javax.annotation.Resource;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+
+/**
+ * 幂等切面
+ *
+ * @author : Charles
+ * @date : 2021/2/26
+ */
+@Slf4j
+@Aspect
+@Component
+@ConditionalOnClass(RedisTemplate.class)
+public class IdempotentAspect {
+
+    private static final String LOCK_SUCCESS = "OK";
+
+    @Resource
+    private RedisTemplate<String,String> redisTemplate;
+
+    /**
+     * 切入点,根据自定义Idempotent实际路径进行调整
+     */
+    @Pointcut("@annotation(com.caimei365.order.idempotent.Idempotent)")
+    public void executeIdempotent() {
+    }
+
+    @Around("executeIdempotent()")
+    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+        // 获取参数对象列表
+        Object[] args = joinPoint.getArgs();
+      	//获取方法
+        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
+        // 得到方法名
+        String methodName = method.getName();
+        // 获取参数数组
+        //Parameter[] parameters = method.getParameters();
+        String[] parameters = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
+
+      	//获取幂等注解
+        Idempotent idempotent = method.getAnnotation(Idempotent.class);
+
+        // 初始化springEL表达式解析器实例
+        ExpressionParser parser = new SpelExpressionParser();
+        // 初始化解析内容上下文
+        EvaluationContext context = new StandardEvaluationContext();
+        // 把参数名和参数值放入解析内容上下文里
+        for (int i = 0; i < parameters.length; i++) {
+            if (args[i] != null) {
+                // 添加解析对象目标
+                context.setVariable(parameters[i], args[i].hashCode());
+            }
+        }
+        // 解析定义key对应的值,拼接成key
+        StringBuilder idempotentKey = new StringBuilder(idempotent.prefix() + ":" + methodName);
+        for (String s : idempotent.keys()) {
+            Arrays.asList(args).contains(s);
+            // 解析对象
+            Expression expression = parser.parseExpression(s);
+            idempotentKey.append(":").append(expression.getValue(context));
+        }
+        // 通过 setnx 确保只有一个接口能够正常访问
+        String result = redisTemplate.execute(
+            (RedisCallback<String>) connection -> (
+                (JedisCommands) connection.getNativeConnection()
+            ).set(
+                idempotentKey.toString(),
+                idempotentKey.toString(),
+                new SetParams().nx().ex(idempotent.expire())
+            )
+        );
+
+        if (LOCK_SUCCESS.equals(result)) {
+            return joinPoint.proceed();
+        } else {
+            log.error("API幂等处理, key=" + idempotentKey);
+            throw new IdempotentException("手速太快了,稍后重试!");
+        }
+    }
+}
+

+ 19 - 0
src/main/java/com/caimei365/order/idempotent/IdempotentException.java

@@ -0,0 +1,19 @@
+package com.caimei365.order.idempotent;
+
+/**
+ * 处理幂等相关异常
+ *
+ * @author : Charles
+ * @date : 2021/2/26
+ */
+public class IdempotentException extends RuntimeException {
+
+    public IdempotentException(String message) {
+        super(message);
+    }
+
+    @Override
+    public String getMessage() {
+        return super.getMessage();
+    }
+}

+ 25 - 0
src/main/java/com/caimei365/order/idempotent/IdempotentExceptionHandler.java

@@ -0,0 +1,25 @@
+package com.caimei365.order.idempotent;
+
+import com.caimei365.order.model.ResponseJson;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+/**
+ * ApI幂等异常处理
+ *
+ * @author : Charles
+ * @date : 2021/6/25
+ */
+@Slf4j
+@ControllerAdvice
+public class IdempotentExceptionHandler {
+    @ExceptionHandler(IdempotentException.class)
+    @ResponseBody
+    public ResponseJson<String> convertExceptionMsg(Exception e) {
+        //自定义逻辑,可返回其他值
+        log.error("ApI幂等错误拦截,错误信息:", e);
+        return ResponseJson.error("幂等异常处理:" + e.getMessage(),null);
+    }
+}

+ 51 - 0
src/main/java/com/caimei365/order/mapper/PriceMapper.java

@@ -0,0 +1,51 @@
+package com.caimei365.order.mapper;
+
+import com.caimei365.order.model.vo.LadderPriceVo;
+import com.caimei365.order.model.vo.PriceVo;
+import com.caimei365.order.model.vo.TaxVo;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2021/4/9
+ */
+@Mapper
+public interface PriceMapper {
+    /**
+     * 根据用户Id查询用户身份
+     */
+    Integer getIdentityByUserId(Integer userId);
+    /**
+     * 根据商品id查找价格
+     */
+    PriceVo getDetailPrice(Integer productId);
+    /**
+     * 根据商品id集合查找 价格列表
+     */
+    List<PriceVo> getListPriceByProductIds(@Param("productIds") List<Integer> productIds);
+    /**
+     * 根据商品ID查询阶梯价列表
+     */
+    List<LadderPriceVo> getLadderPricesByProductId(Integer productId);
+    /**
+     * 获取最低阶梯价(价格最低,阶梯数最大)
+     */
+    LadderPriceVo findLowerLadderPrice(Integer productId);
+    /**
+     * 获取最高阶梯价(价格最高,阶梯数最小)
+     */
+    LadderPriceVo findMaxLadderPrice(Integer productId);
+    /**
+     * 根据商品ID和用户ID 查询复购价
+     */
+    Double getRepurchasePrice(@Param("productId") Integer productId, @Param("userId") Integer userId);
+    /**
+     * 根据商品ID含税和发票信息
+     */
+    TaxVo getTaxByProductId(Integer productId);
+}

+ 85 - 0
src/main/java/com/caimei365/order/model/ResponseJson.java

@@ -0,0 +1,85 @@
+package com.caimei365.order.model;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 全局API返回值
+ *
+ * @author : Charles
+ * @date : 2021/3/4
+ */
+@Data
+public class ResponseJson<T> implements Serializable {
+    /** 状态码 */
+    @ApiModelProperty("状态码")
+    private int code;
+    /** 提示信息 */
+    @ApiModelProperty("提示信息")
+    private String msg;
+    /** 返回的数据 */
+    @ApiModelProperty("响应数据")
+    private T data;
+
+    private ResponseJson() {}
+
+    private ResponseJson(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    private ResponseJson(int code, String msg, T data) {
+        this.code = code;
+        this.msg = msg;
+        this.data = data;
+    }
+
+    public static ResponseJson success() {
+        return new ResponseJson<>(0, "操作成功");
+    }
+
+    public static<T> ResponseJson<T> success(T data) {
+        return new ResponseJson<>(0, "操作成功", data);
+    }
+
+    public static<T> ResponseJson<T> success(String msg, T data) {
+        return new ResponseJson<>(0, msg, data);
+    }
+
+    public static<T> ResponseJson<T> success(int code, String msg, T data) {
+        return new ResponseJson<>(code, msg, data);
+    }
+
+    public static ResponseJson error() {
+        return new ResponseJson<>(-1, "操作失败");
+    }
+
+    public static ResponseJson error(String msg) {
+        return new ResponseJson<>(-1, msg);
+    }
+
+    public static ResponseJson error(int code, String msg) {
+        return new ResponseJson<>(code, msg);
+    }
+
+    public static<T> ResponseJson<T> error(T data) {
+        return new ResponseJson<>(-1, "操作失败", data);
+    }
+
+    public static<T> ResponseJson<T> error(String msg, T data) {
+        return new ResponseJson<>(-1, msg, data);
+    }
+
+    public static<T> ResponseJson<T> error(int code, String msg, T data) {
+        return new ResponseJson<>(code, msg, data);
+    }
+
+    @Override
+    public String toString() {
+        return "ResponseJson{" + "code=" + code + ", msg='" + msg + '\'' + ", data=" + data + '}';
+    }
+
+    private static final long serialVersionUID = 1L;
+}

+ 43 - 0
src/main/java/com/caimei365/order/model/vo/LadderPriceVo.java

@@ -0,0 +1,43 @@
+package com.caimei365.order.model.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 阶梯价格
+ *
+ * @author : Charles
+ * @date : 2021/4/9
+ */
+@Data
+public class LadderPriceVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Integer id;
+    /**
+     * 商品id
+     */
+    private Integer productId;
+    /**
+     * 第几阶梯
+     */
+    private Integer ladderNum;
+    /**
+     * 购买数量
+     */
+    private Integer buyNum;
+    /**
+     * 购买价格
+     */
+    private Double buyPrice;
+    /**
+     * 下一阶数量
+     */
+    private Integer maxNum;
+    /**
+     * 显示数量 如:1~3
+     */
+    private String numRange;
+
+}
+

+ 101 - 0
src/main/java/com/caimei365/order/model/vo/PriceVo.java

@@ -0,0 +1,101 @@
+package com.caimei365.order.model.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 商品价格
+ *
+ * @author : Charles
+ * @date : 2021/4/9
+ */
+@Data
+public class PriceVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * 商品productID
+     */
+    private Integer productId;
+    /**
+     * 供应商ID
+     */
+    private Integer shopId;
+    /**
+     * 价格(计算后)
+     */
+    private Double price;
+    /**
+     * 划线价格
+     */
+    private Double originalPrice;
+    /**
+     * 最小购买量
+     */
+    private Integer minBuyNumber;
+    /**
+     * 最大购买量
+     */
+    private Integer maxBuyNumber;
+    /**
+     * 是否公开机构价 0公开价格 1不公开价格
+     */
+    private Integer priceFlag;
+    /**
+     * 是否复购 0否 1是
+     */
+    private Integer repurchaseFlag;
+    /**
+     * 阶梯价标志
+     */
+    private Integer ladderPriceFlag;
+    /**
+     * 市场价
+     */
+    private Double normalPrice;
+    /**
+     * 成本价
+     */
+    private Double costPrice;
+    /**
+     * 比例成本百分比
+     */
+    private Double costProportional;
+    /**
+     * 成本价选中标志:1固定成本 2比例成
+     */
+    private Integer costCheckFlag;
+    /**
+     * 用户身份: 2-会员机构, 4-普通机构
+     */
+    private Integer userIdentity;
+
+    /**
+     * 购买数量: 1逐步增长,2以起订量增长(起订量的倍数增长)
+     */
+    private Integer step;
+    /**
+     * 商品是否处于活动状态 1是 0否
+     */
+    private Integer actStatus;
+    /**
+     * 促销活动
+     */
+    private PromotionsVo promotions;
+    /**
+     * 机构税率
+     */
+    private BigDecimal taxRate;
+    /**
+     * 是否含税 0不含税,1含税,2未知
+     */
+    private String includedTax;
+
+    /**
+     * 发票类型(基于是否含税基础) 1增值税票,2普通票, 3不能开票
+     */
+    private String invoiceType;
+
+}
+

+ 78 - 0
src/main/java/com/caimei365/order/model/vo/PromotionsVo.java

@@ -0,0 +1,78 @@
+package com.caimei365.order.model.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 促销活动
+ *
+ * @author : Charles
+ * @date : 2021/4/9
+ */
+@Data
+public class PromotionsVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Integer id;
+    /**
+     * 促销名称
+     */
+    private String name;
+    /**
+     * 促销描述
+     */
+    private String description;
+    /**
+     * 分类: 1单品促销,2凑单促销,3店铺促销
+     */
+    private Integer type;
+    /**
+     * 促销方式:1优惠,2满减,3满赠
+     */
+    private Integer mode;
+    /**
+     * 优惠价/满减/满赠的设定价格(如满999赠商品)
+     */
+    private Double touchPrice;
+    /**
+     * 减免价格
+     */
+    private Double reducedPrice;
+    /**
+     * 开始时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date beginTime;
+    /**
+     * 结束时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date endTime;
+    /**
+     * 时效:1永久,2区间过期,其它无效
+     */
+    private Integer status;
+    /**
+     * 店铺id(店铺促销时供应商ID)
+     */
+    private Integer shopId;
+    /**
+     * 商品id
+     */
+    private Integer productId;
+    /**
+     * 主订单id
+     */
+    private Integer orderId;
+//    /**
+//     * 该优惠下商品
+//     */
+//    private List<ProductItemVo> productList;
+//    /**
+//     * 该优惠下赠品品
+//     */
+//    private List<ProductItemVo> giftList;
+}

+ 28 - 0
src/main/java/com/caimei365/order/model/vo/TaxVo.java

@@ -0,0 +1,28 @@
+package com.caimei365.order.model.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2021/4/9
+ */
+@Data
+public class TaxVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * 发票类型(基于是否含税基础)   1增值税票,2普通票, 3不能开票
+     */
+    private Integer invoiceType;
+    /**
+     * 是否含税   0不含税,1含税,2未知
+     */
+    private Integer includedTax;
+    /**
+     * 开票税点(基于不含税基础) :增值税默认13%,普通票6%取值范围[0-100]
+     */
+    private Double taxPoint;
+}

+ 113 - 0
src/main/java/com/caimei365/order/utils/JwtUtil.java

@@ -0,0 +1,113 @@
+package com.caimei365.order.utils;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.interfaces.JWTVerifier;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JWT工具类
+ *
+ * 标准中注册的声明
+ * iss: jwt签发者
+ * sub: jwt所面向的用户
+ * aud: 接收jwt的一方
+ * exp: jwt的过期时间,这个过期时间必须要大于签发时间
+ * nbf: 定义在什么时间之前,该jwt都是不可用的.
+ * iat: jwt的签发时间
+ * jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
+ *
+ *
+ * @author : Charles
+ * @date : 2021/3/8
+ */
+public class JwtUtil {
+    /**
+     * 设置过期时间: 2*60分钟
+     */
+    private static final long EXPIRE_TIME = 2 * 60 * 60 * 1000;
+    /**
+     * 服务端的私钥secret,在任何场景都不应该流露出去
+     */
+    private static final String TOKEN_SECRET = "zhengchao";
+
+    /**
+     * 生成签名,6EXPIRE_TIME过期
+     */
+    public static String createToken(Integer userId) {
+        try {
+            // 设置过期时间
+            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
+            // 私钥和加密算法
+            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
+            // 设置头部信息
+            Map<String, Object> header = new HashMap<>(2);
+            header.put("typ", "JWT");
+            header.put("alg", "HS256");
+            // 返回token字符串
+            return JWT.create()
+                    .withHeader(header)
+                    .withClaim("uid", userId)
+                    /*.withClaim("aud", mobile)*/
+                    .withExpiresAt(date)
+                    .sign(algorithm);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 检验token是否正确
+     */
+    public static boolean isVerify(String token) {
+        try {
+            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
+            JWTVerifier verifier = JWT.require(algorithm).build();
+            verifier.verify(token);
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 从token解析出uid信息,用户ID
+     */
+    public static int parseTokenUid(String token) {
+        DecodedJWT jwt = JWT.decode(token);
+        return jwt.getClaim("uid").asInt();
+    }
+
+    /**
+     * 从token解析出过期日期时间
+     */
+    public static Date paraseExpiresAt(String token) {
+        DecodedJWT jwt = JWT.decode(token);
+        return jwt.getExpiresAt();
+    }
+
+    /**
+     * 返回设置的过期秒数
+     *
+     * @return long 秒数
+     */
+    public static long getExpireTime() {
+        return EXPIRE_TIME / 1000;
+    }
+
+//    /**
+//     * 从token解析出aud信息,用户名
+//     *
+//     * @param token
+//     * @return
+//     */
+//    public static String parseTokenAud(String token) {
+//        DecodedJWT jwt = JWT.decode(token);
+//        return jwt.getClaim("aud").asString();
+//    }
+}

+ 15 - 0
src/main/resources/bootstrap.yml

@@ -0,0 +1,15 @@
+server:
+  port: 18012
+
+# 指定当前服务的名称,这个名称会注册到注册中心
+spring:
+  application:
+    name: @artifactId@
+  cloud:
+    config:                             # Config客户端配置
+      profile: @activatedProperties@    # 启用配置后缀名称
+      label: master                     # 分支名称
+      # uri: http://localhost:18001
+      # uri: http://47.119.112.46:18001       # 配置中心地址
+      uri: http://120.79.162.1:18001          # 配置中心地址(正式环境)
+      name: order                       # 配置文件名称

+ 94 - 0
src/main/resources/mapper/PriceMapper.xml

@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.caimei365.order.mapper.PriceMapper">
+    <select id="getIdentityByUserId" resultType="java.lang.Integer">
+        select userIdentity from user where userID = #{userId}
+    </select>
+    <select id="getDetailPrice" resultType="com.caimei365.order.model.vo.PriceVo">
+        select
+            p.productID as productId,
+            p.actStatus,
+            p.price1 as price,
+            p.minBuyNumber,
+            p.maxBuyNumber,
+            p.price1TextFlag as priceFlag,
+            p.ladderPriceFlag,
+            p.normalPrice,
+            p.costPrice,
+            p.costProportional,
+            p.costCheckFlag,
+            p.step,
+            p.shopID as shopId,
+            p.taxPoint as taxRate,
+            p.includedTax,
+            p.invoiceType
+        from product p
+        where productID = #{productId}
+    </select>
+    <select id="getListPriceByProductIds" resultType="com.caimei365.order.model.vo.PriceVo">
+        select
+            p.productID as productId,
+            p.actStatus,
+            p.price1 as price,
+            p.minBuyNumber,
+            p.maxBuyNumber,
+            p.price1TextFlag as priceFlag,
+            p.ladderPriceFlag,
+            p.normalPrice,
+            p.costPrice,
+            p.costProportional,
+            p.costCheckFlag,
+            p.step,
+            p.shopID as shopId,
+            p.includedTax,
+            p.invoiceType,
+            p.taxPoint as taxRate
+        from product p
+        where productID in
+          <foreach collection="productIds" open="(" separator="," close=")" item="productId">
+              #{productId}
+          </foreach>
+    </select>
+    <select id="getLadderPricesByProductId" resultType="com.caimei365.order.model.vo.LadderPriceVo">
+        select
+        	id, productId, ladderNum, buyNum, buyPrice
+        from product_ladder_price
+        where productId = #{productId} and userType = 3 and delFlag = 0
+        order by ladderNum asc
+    </select>
+    <select id="findLowerLadderPrice" resultType="com.caimei365.order.model.vo.LadderPriceVo">
+        select
+        	id, productId, ladderNum, buyNum, buyPrice
+        from product_ladder_price
+        where productId = #{productId} and userType = 3 and delFlag = 0
+        order by ladderNum DESC
+        limit 1
+    </select>
+    <select id="findMaxLadderPrice" resultType="com.caimei365.order.model.vo.LadderPriceVo">
+        select
+        	id, productId, ladderNum, buyNum, buyPrice
+        from product_ladder_price
+        where productId = #{productId} and userType = 3 and delFlag = 0
+        order by ladderNum asc
+        limit 1
+    </select>
+    <select id="getRepurchasePrice" resultType="java.lang.Double">
+        select
+          r.currentPrice
+        from repeat_purchase_price r
+        left join product p on p.productID = r.productId
+        where r.productId = #{productId} and userId = #{userId}
+        and ((p.costCheckFlag=1 and r.currentPrice <![CDATA[ >= ]]> p.costPrice) or p.costCheckFlag=2)
+        and p.price1 <![CDATA[ >= ]]> r.currentPrice
+        and r.delFlag = 0
+    </select>
+    <select id="getTaxByProductId" resultType="com.caimei365.order.model.vo.TaxVo">
+        select
+        p.includedTax as includedTax,
+        p.invoiceType as invoiceType,
+        p.taxPoint as taxPoint
+        from product p
+        where productID = #{productId}
+    </select>
+
+</mapper>

+ 20 - 0
src/test/java/com/caimei365/order/OrderApplicationTests.java

@@ -0,0 +1,20 @@
+package com.caimei365.order;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * Description
+ *
+ * @author : Charles
+ * @date : 2021/6/25
+ */
+@SpringBootTest
+class OrderApplicationTests {
+
+    @Test
+    void contextLoads() {
+
+    }
+
+}