Commit f94d388b authored by shangtx's avatar shangtx

init: 基础代码

parents
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
logs
<?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">
<parent>
<artifactId>parent</artifactId>
<groupId>com.onsiteservice</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>admin</artifactId>
<description>Admin服务端</description>
<dependencies>
<!-- 业务通用模块组-->
<dependency>
<groupId>com.onsiteservice</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build>
<finalName>admin</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<fork>true</fork>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
\ No newline at end of file
package com.onsiteservice.admin;
import com.onsiteservice.constant.constant.Constants;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(value = {Constants.PACKAGE_NAME_GROUP + ".**"})
public class OnSiteServiceAdminApplication {
public static void main(String[] args) {
SpringApplication.run(OnSiteServiceAdminApplication.class, args);
}
}
# 开发环境配置
spring:
profiles:
include:
- common-dev
# 生产环境配置
spring:
profiles:
include:
- common-prod
#
#logging:
# config: classpath:logback-spring-prod.xml
project:
swagger:
enabled: false
# 联调环境配置
spring:
profiles:
include:
- common-sit
# 测试环境配置
spring:
profiles:
include:
- common-test
# 所有环境通用的配置
server:
port: 8297
# 所有自定义项目工程配置值 在project下配置
project:
swagger:
enabled: true
title: 城市服务预约Web管理端${spring.profiles.active}环境
description: 城市服务预约Web管理端API文档
base-package: com.onsiteservice.admin.controller,com.onsiteservice.common.business.controller
jwt:
enabled: true # 是否开启jwt拦截
secret: b25zaXRlLXNlcnZpY2UtYWRtaW4tYmFzZTY0LXNlY3JldA==
issuer: onsite-service-admin
expires-time: 86400 # 1天有效期 秒
admin-security:
enabled: true
custom-security: false # url级别验证开关
# 无需授权url资源
ant-paths: "/error,/websocket/**,/api,/token/base,/monitor/**,\
/,/dict,/login/init,/login,/sys/user/info"
spring:
profiles:
include:
- common
application:
name: onsite-service-admin
package com.onsiteservice.admin;
import org.jasypt.encryption.StringEncryptor;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
public class JasyptTest {
@Resource
private StringEncryptor encryptor;
@Test
void encrypt() {
String password = encryptor.encrypt("antaikeji2022");
System.out.println(password);
}
}
<?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">
<parent>
<artifactId>parent</artifactId>
<groupId>com.onsiteservice</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common</artifactId>
<description>公共通用模块</description>
<dependencies>
<!--依赖核心模块-->
<dependency>
<groupId>com.onsiteservice</groupId>
<artifactId>core</artifactId>
<version>1.0.0</version>
</dependency>
<!--依赖数据库层和模型层模块-->
<dependency>
<groupId>com.onsiteservice</groupId>
<artifactId>dao</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Redis缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis新异步spring boot 2.x默认客户端lettuce依赖需要 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- SpringBoot集成RabbitMQ消息队列 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- WebSocket服务器推送 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--spring session 分布式session共享 开启redis不然启动Spring boot会报错 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- SpringFox-Swagger-UI 用于文档默认界面展示 减少jar大小 更好的交互和UI建议使用纯前端UI -->
<!-- <dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.8</version>
</dependency>
<!-- 阿里云核心服务 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.20</version>
</dependency>
<!-- 阿里云短信服务SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
<!-- 阿里云OSS存储服务SDK -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.12.0</version>
</dependency>
<!-- Excel等导入导出功能来加快数据的操作 -->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
<!-- 极光推送服务端SDK -->
<dependency>
<groupId>cn.jpush.api</groupId>
<artifactId>jpush-client</artifactId>
<version>3.4.8</version>
</dependency>
<!-- Java 操作二维码生成与解析 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
<!-- 中文拼音工具包 -->
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
<!-- 邮件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- dom4j 适用于Java的灵活XML操作框架 -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!--阿里云日志服务-->
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>aliyun-log-logback-appender</artifactId>
<version>0.1.19</version>
</dependency>
<!-- 微信小程序 starter配合yaml配置需要在各自的模块中 -->
<!-- <dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
</dependency>-->
<!--微信公众号-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
</dependency>
<!--微信支付-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-pay-spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Prometheus监控 https://prometheus.io/docs/introduction/overview/ -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.7.5</version>
</dependency>
</dependencies>
<build>
<finalName>common</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<fork>true</fork>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
\ No newline at end of file
package com.onsiteservice.common.annotation.idempotent;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author 潘维吉
* @date 2020/6/29 17:46
* @email 406798106@qq.com
* @description 在需要保证 接口幂等性 的Controller的方法上使用此注解
* 注意查询和删除是天然的幂等性 不需要添加 新增和修改的接口添加
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
long interval() default 1000L; // 默认1s 1s之内只能访问一次
}
package com.onsiteservice.common.annotation.idempotent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* @author 潘维吉
* @date 2020/6/29 17:47
* @email 406798106@qq.com
* @description 接口幂等性拦截器
*/
@Component
@Slf4j
public class ApiIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private ApiIdempotentService apiIdempotentService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
if (methodAnnotation != null) {
apiIdempotentService.checkToken(request, methodAnnotation.interval());// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
package com.onsiteservice.common.annotation.idempotent;
import com.onsiteservice.common.redis.RedisUtils;
import com.onsiteservice.core.exception.ServiceException;
import com.onsiteservice.core.security.jwt.JwtManager;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
/**
* @author 潘维吉
* @date 2020/6/29 18:01
* @email 406798106@qq.com
* @description 幂等性服务
*/
@Service
public class ApiIdempotentService {
@Autowired
private RedisUtils redisUtils;
/**
* 创建token 并定时缓存
*/
public void createToken(String businessToken, Long interval) {
if (!redisUtils.exists(businessToken)) {
redisUtils.set(businessToken, businessToken, interval, TimeUnit.MILLISECONDS);
}
}
/**
* 验证token
*/
public void checkToken(HttpServletRequest request, Long interval) {
String token = request.getHeader(JwtManager.AUTHORIZATION_HEADER).replace(JwtManager.BEARER, "");
if (StringUtils.isBlank(token)) { // header中不存在token
token = request.getParameter("token");
if (StringUtils.isBlank(token)) { // parameter中也不存在token
throw new ServiceException("用户token不存在");
}
}
// 业务token唯一标识
String businessToken = token + "-" + request.getMethod() + "-" + request.getRequestURI();
if (redisUtils.exists(businessToken)) {
throw new ServiceException("重复执行请求");
}
// 检测成功后后重新创建token缓存
createToken(businessToken, interval);
}
}
package com.onsiteservice.common.annotation.limit;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @author 潘维吉
* @date 2020/3/13 10:45
* @email 406798106@qq.com
* @description API接口限流注解
* 令牌桶算法 会以一个恒定的速率向固定容量大小桶中放入令牌,当有浏览来时取走一个或者多个令牌
* 当发生高并发情况下拿到令牌的执行业务逻辑,没有获取到令牌的就会丢弃获取服务降级处理,提示一个友好的错误信息给用户
* @RateLimit(permitsPerSecond = 1, msg = "亲 , 现在流量过大 , 请稍后再试")
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RateLimit {
// 以固定数值往令牌桶添加令牌 每秒不要超过多少个
double permitsPerSecond() default 10;
// 获取令牌最大等待时间
long timeout() default 500;
// 单位(例:分钟/秒/毫秒) 默认:毫秒
TimeUnit timeunit() default TimeUnit.MILLISECONDS;
// 无法获取令牌返回提示信息 默认值可以自行修改
String msg() default "系统繁忙 , 请稍后再试";
}
package com.onsiteservice.common.annotation.limit;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import com.onsiteservice.core.config.MvcConfig;
import com.onsiteservice.core.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
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.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import static com.onsiteservice.core.result.ResultGenerator.fail;
/**
* @author 潘维吉
* @date 2020/3/13 10:48
* @email 406798106@qq.com
* @description AOP的环绕通知来拦截注解 使用Guava RateLimiter本地限流
* 使用了一个ConcurrentMap来保存每个请求对应的令牌桶,key是没有url请求,防止出现每个请求都会新建一个令牌桶这么会达不到限流效果
*/
@ConditionalOnProperty(prefix = "project.rate-limit", name = {"enabled"}, matchIfMissing = false)
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
/**
* 使用url做为key,存放令牌桶 防止每次重新创建令牌桶
*/
private Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();
@Pointcut("@annotation(com.monorepo.common.annotation.limit.RateLimit)")
public void rateLimit() {
}
@Around("rateLimit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取request,response
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
// 或者url(存在map集合的key)
String url = request.getRequestURI();
// 获取自定义注解
RateLimit rateLimiter = getRateLimit(joinPoint);
if (rateLimiter != null) {
RateLimiter limiter = null;
// 判断map集合中是否有创建有创建好的令牌桶
if (!limitMap.containsKey(url)) {
// 创建令牌桶 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
// 当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
limiter = RateLimiter.create(rateLimiter.permitsPerSecond());
limitMap.put(url, limiter);
log.info("请求{} , 创建令牌桶 , 容量permitsPerSecond={} 成功", url, rateLimiter.permitsPerSecond());
}
limiter = limitMap.get(url);
// 获取令牌 带超时时间
boolean acquire = limiter.tryAcquire(rateLimiter.timeout(), rateLimiter.timeunit());
if (!acquire) {
Result result = fail(rateLimiter.msg());
MvcConfig.responseResult(response, result);
return null;
}
}
return joinPoint.proceed();
}
/**
* 获取注解对象
*
* @param joinPoint 对象
*/
private RateLimit getRateLimit(final JoinPoint joinPoint) {
Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods();
String name = joinPoint.getSignature().getName();
if (!StringUtils.isEmpty(name)) {
for (Method method : methods) {
RateLimit annotation = method.getAnnotation(RateLimit.class);
if (!Objects.isNull(annotation) && name.equals(method.getName())) {
return annotation;
}
}
}
return null;
}
}
package com.onsiteservice.common.annotation.user;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author 潘维吉
* @date 2018-08-20
* 方法参数中使用此注解,该方法在映射时会注入当前登录的User对象的id
* @CurrentUserId Long(Integer) userId 方式注入方法参数
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUserId {
}
package com.onsiteservice.common.annotation.user;
import com.onsiteservice.constant.constant.Constants;
import com.onsiteservice.core.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* @author 潘维吉
* @date 2018-08-20
* 自定义解析器实现请求参数绑定方法参数
* 增加方法注入,将含有CurrentUserId注解的方法参数注入当前请求用户
* JwtInterceptor拦截器 已将userId存入request中
* @CurrentUserId Long(Integer) userId 方式注入方法参数
*/
@Component
@Slf4j
public class CurrentUserIdResolver implements HandlerMethodArgumentResolver {
/**
* 判定是否需要处理该参数分解,返回true为需要,并会去调用下面的resolveArgument方法
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
//如果参数有@CurrentUserId注解则支持
if ((paramType.isAssignableFrom(Long.class) || paramType.isAssignableFrom(Integer.class)) &&
parameter.hasParameterAnnotation(CurrentUserId.class)) {
return true;
}
return false;
}
/**
* 真正用于处理参数分解的方法,返回的类型就是controller方法上的形参对象
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
try {
//获取jwt鉴权时存入Request的请求用户的Id
Object currentUserId = webRequest.getAttribute(Constants.CURRENT_USER_ID,
RequestAttributes.SCOPE_REQUEST);
String parameterTypeName = parameter.getParameterType().getName();
if (ObjectUtils.isEmpty(currentUserId)) {
// 先确认jwt拦截器 是否将数据成功放到Request中
throw new ServiceException("CURRENT_USER_ID数据不存在");
} else if ("java.lang.Long".equals(parameterTypeName)) {
return Long.valueOf(currentUserId.toString());
} else if ("java.lang.Integer".equals(parameterTypeName)) {
return Integer.valueOf(currentUserId.toString());
} else if ("java.lang.String".equals(parameterTypeName)) {
return currentUserId.toString();
} else {
throw new ServiceException("@CurrentUserId参数类型错误 支持Long或Integer或String");
}
} catch (Exception e) {
log.error("异常信息: " + e.getMessage());
throw new RuntimeException("CurrentUserIdResolver获取当前请求用户id异常");
}
}
}
package com.onsiteservice.common.annotation.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author 潘维吉
* @date 2019-01-02 11:47
* 自定义验证注解
* @Constraint 指明校验逻辑的类
*/
@Documented
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = DateTimeValidator.class)
public @interface DateTime {
/**
* 错误消息
*
* @return 默认错误消息
*/
String message() default "时间格式错误";
/**
* 时间格式
*
* @return 验证的日期格式
*/
String format() default "yyyy-MM-dd";
/**
* 允许我们为约束指定验证组
*
* @return 分组
*/
Class<?>[] groups() default {};
/**
* payload
*
* @return
*/
Class<? extends Payload>[] payload() default {};
}
package com.onsiteservice.common.annotation.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* @author 潘维吉
* @date 2019-01-02 11:55
* 自定义日期格式验证
* <DateTime, String> DateTime校验注解的类型 String要对什么类型的字段进行校验
*/
public class DateTimeValidator implements ConstraintValidator<DateTime, String> {
private DateTime dateTime;
@Override
public void initialize(DateTime dateTime) {
this.dateTime = dateTime;
}
/**
* 校验的逻辑
*
* @param value
* @param context
* @return
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果 value 为空则不进行格式验证,为空验证可以使用 @NotBlank @NotNull @NotEmpty 等注解来进行控制,职责分离
if (value == null) {
return true;
}
String format = dateTime.format();
if (value.length() != format.length()) {
return false;
}
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
try {
simpleDateFormat.parse(value);
} catch (ParseException e) {
return false;
}
return true;
}
}
package com.onsiteservice.common.annotation.validation;
import java.lang.annotation.*;
/**
* 仅用于简单情况下的重复校验
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Unique {
/**
* 要校验的字段
*/
String[] field();
/**
* 条件字段,会使用等于作为条件
*/
String[] conditionFields() default {};
/**
* 查出重复后的提示消息
*/
String[] message() default "数据已存在";
}
package com.onsiteservice.common.annotation.validation;
import com.onsiteservice.core.exception.ServiceException;
import com.onsiteservice.dao.common.AbstractMapper;
import com.onsiteservice.util.ReflectUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Condition;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
@Slf4j
@Aspect
@Component
public class UniqueCheckAspect {
@Pointcut("@annotation(com.monorepo.common.annotation.validation.Unique)")
public void unique() {
}
@Before("unique()")
public void before(JoinPoint joinPoint) {
var obj = joinPoint.getArgs()[0]; // 获取参数对象
AbstractMapper mapper = (AbstractMapper) joinPoint.getThis(); // 获取service
var uniqueCheck = getUniqueCheck(joinPoint); // 获取注解对象
String[] field = uniqueCheck.field();
var conditionFields = uniqueCheck.conditionFields();
var message = uniqueCheck.message();
var modelClass = obj.getClass();
while (!isModel(modelClass)) {
modelClass = modelClass.getSuperclass();
}
for(int i=0;i<field.length; i++) {
String f = field[i];
Condition condition = new Condition(modelClass);
Condition.Criteria criteria = condition.createCriteria();
try {
criteria.andEqualTo(f, BeanUtils.getNestedProperty(obj, f));
Object id = ReflectUtils.invokeGetter(obj, "id");
if (ObjectUtils.isNotEmpty(id)) {
// 有id,在除了该条数据外的所有数据中查询
criteria.andNotEqualTo("id", id);
}
for (String conditionField : conditionFields) {
criteria.andEqualTo(conditionField, ReflectUtils.invokeGetter(obj, conditionField));
}
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
e.printStackTrace();
}
if (mapper.selectCountByCondition(condition) != 0) {
throw new ServiceException(message[i]);
}
}
}
/**
* 判断某类型是不是Model, 根据有没有id字段判断
*/
private boolean isModel(Class<?> clazz) {
return List.of(clazz.getDeclaredFields()).stream().anyMatch(f -> "id".equals(f.getName()));
}
/**
* 获取注解对象
*
* @param joinPoint 对象
*/
private Unique getUniqueCheck(final JoinPoint joinPoint) {
Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods();
String name = joinPoint.getSignature().getName();
if (!StringUtils.isEmpty(name)) {
for (Method method : methods) {
Unique annotation = method.getAnnotation(Unique.class);
if (!Objects.isNull(annotation) && name.equals(method.getName())) {
return annotation;
}
}
}
return null;
}
}
package com.onsiteservice.common.config;
import com.onsiteservice.common.annotation.idempotent.ApiIdempotentInterceptor;
import com.onsiteservice.common.annotation.user.CurrentUserIdResolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.util.List;
/**
* @author 潘维吉
* @date 2019-05-20
* MVC 通用统一配置
*/
@Configuration
@Slf4j
public class CommonConfig implements WebMvcConfigurer {
@Resource
private ApiIdempotentInterceptor apiIdempotentInterceptor;
@Resource
private CurrentUserIdResolver currentUserIdResolver;
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiIdempotentInterceptor);
}
/**
* 添加参数解析器
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(currentUserIdResolver);
}
/**
* 添加全局请求参数类型转换器和格式化器
* 通过覆盖addFormatter方法来实现对Converter和ConverterFactory的绑定
*
* @param registry
*/
@Override
public void addFormatters(FormatterRegistry registry) {
//给当前的Spring容器中添加自定义格式转换器
// registry.addConverter(new StringToDateConverter());
// registry.addConverter(new BooleanToNumberConverter());
}
}
package com.onsiteservice.common.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 潘维吉
* @date 2020-01-12
* RabbitMQ消息队列配置
* AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,
* 是应用层协议的一个开放标准,为面向消息的中间件设计。
* 消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
* AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
* 主要的功能是用来实现应用服务的异步与解耦,同时也能起到削峰填谷、消息分发的作用
*/
@ConditionalOnProperty(prefix = "project.rabbitmq", name = {"enabled"}, matchIfMissing = false)
@Configuration
@EnableRabbit
@Slf4j
public class RabbitMQConfig {
/** 配置RabbitMQ消息服务 */
@Autowired
public ConnectionFactory connectionFactory;
/**
* ConnectionFactory类似于数据连接等
*
* @return SimpleRabbitListenerContainerFactory
*/
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
// 消息转换器, 如将Java类转换JSON类型发送至Broker, 从Broker处获取JSON消息转换为Java类型
factory.setMessageConverter(new Jackson2JsonMessageConverter());
// 注意 开启ACK NONE自动确认 MANUAL手动确认 AUTO根据情况确认
factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(50);
return factory;
}
/**
* RabbitTemplate,用来发送消息
*
* @return RabbitTemplate
*/
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(new Jackson2JsonMessageConverter());
return template;
}
/**
* 手动定义消息队列
* 也可以根据@RabbitListener(queuesToDeclare = @Queue()) 自动创建队列
*/
/* @Bean
public Queue messageQueue() {
return new Queue(SysConstants.Queue.MESSAGE, true);
}*/
/**
* 定义消息队列
*
* @return Queue
*/
/* @Bean
public Queue queue() {
// 创建字符串消息队列 boolean值代表是否持久化消息
//durable 是否消息持久化
//autoDelete 是否自动删除,即服务端或者客服端下线后 交换机自动删除
return new Queue(QUEUE_NAME, true);
}*/
/**
* Direct交换机
*
* 最为简单 先策略匹配到对应绑定的队列后 才会被投送到该队列 交换机跟队列必须是精确的对应关系
*
* @return DirectExchange
*/
/* @Bean
public DirectExchange directExchange() {
return new DirectExchange(EXCHANGE_NAME, true, false);
}*/
/**
* 创建 topic 类型的交换器
* 交换机(Exchange) 描述:接收消息并且转发到绑定的队列,交换机不存储消息
*
* @return TopicExchange
*/
/* @Bean
public TopicExchange topicExchange() {
return new TopicExchange(EXCHANGE_NAME, true, false);
}*/
/**
* 建立交换机与队列的绑定关系
*
* @return
*/
/* @Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(topicExchange()).with(ROUTING_KEY);
}*/
/**
* 需要将ACK修改为手动确认,避免消息在处理过程中发生异常造成被误认为已经成功消费的假象
* 使用@RabbitListener注释代替
* @return SimpleMessageListenerContainer
*/
/* @Bean
public SimpleMessageListenerContainer messageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 监听的队列
container.setQueues(queue());
container.setExposeListenerChannel(true);
container.setConcurrentConsumers(1);
container.setMaxConcurrentConsumers(10);
//开启ACK NONE自动确认 MANUAL手动确认 AUTO根据情况确认
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 消息监听处理
container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {
byte[] body = message.getBody();
log.info("RabbitMQ消费端接收到消息 : " + new String(body));
if (message.getMessageProperties().getHeaders().get("error") == null) {
// 确认消息成功消费
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
log.info("RabbitMQ消息已经确认");
} else {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
log.info("RabbitMQ消息拒绝");
}
});
return container;
}*/
}
package com.onsiteservice.common.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* @author 潘维吉
* @date 2021/1/7 12:57
* @email 406798106@qq.com
* @description session分布式共享配置
*/
@Configuration
@ConditionalOnProperty(prefix = "project.session-shared", name = {"enabled"}, matchIfMissing = true)
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
}
package com.onsiteservice.common.config;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.Servlet;
import java.io.IOException;
/**
* @author 潘维吉
* @date 2020/8/7 9:51
* @email 406798106@qq.com
* @description 自动去掉字符串url、form(x-www-form-urlencoded)表单、@RequestBody json(raw)中的入参前后空格配置
*/
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
public class StringTrimConfig {
@ControllerAdvice
public static class ControllerStringParamTrimConfig {
@InitBinder
public void initBinder(WebDataBinder binder) {
// 创建 String trim 编辑器
// 构造方法中 boolean 参数含义为如果是空白字符串,是否转换为null
// 即如果为true,那么 " " 会被转换为 null,否者为 ""
StringTrimmerEditor propertyEditor = new StringTrimmerEditor(false);
// 为 String 类对象注册编辑器
binder.registerCustomEditor(String.class, propertyEditor);
}
}
/**
* 接收@RequestBody中JSON或XML对象参数
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return new Jackson2ObjectMapperBuilderCustomizer() {
@Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder
// 为 String 类型自定义反序列化操作
.deserializerByType(String.class, new StdScalarDeserializer<Object>(String.class) {
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext ctx)
throws IOException {
// 去除前后空格
return StringUtils.trimWhitespace(jsonParser.getValueAsString());
}
});
}
};
}
}
package com.onsiteservice.common.converter;
import org.springframework.core.convert.converter.Converter;
import java.util.HashSet;
import java.util.Set;
/**
* @author 潘维吉
* @date 2019-03-25 10:52
* 请求参数Boolean数据类型转换Number类型
* true false 转成成 1 0
*/
public class BooleanToNumberConverter implements Converter<String, Byte> {
private static Set<String> trueValues = new HashSet(4);
private static Set<String> falseValues = new HashSet(4);
static {
trueValues.add("true");
falseValues.add("false");
}
@Override
public Byte convert(String source) {
String value = source.trim();
if (value.isEmpty()) {
return null;
} else {
if (trueValues.contains(value)) {
return 1;
} else if (falseValues.contains(value)) {
return 0;
} else {
throw new IllegalArgumentException("请求参数Boolean数据类型转换Number型异常: value '" + source + "'");
}
}
}
}
package com.onsiteservice.common.converter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.convert.converter.Converter;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author 潘维吉
* @date 2019-03-25 12:15
* 请求参数String时间数据类型转换Date类型
*/
public class StringToDateConverter implements Converter<String, Date> {
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final String SHORT_DATE_FORMAT = "yyyy-MM-dd";
@Override
public Date convert(String source) {
if (StringUtils.isBlank(source)) {
return null;
}
source = source.trim();
try {
if (source.contains("-")) {
SimpleDateFormat formatter;
if (source.contains(":")) {
formatter = new SimpleDateFormat(DATE_FORMAT);
} else {
formatter = new SimpleDateFormat(SHORT_DATE_FORMAT);
}
return formatter.parse(source);
} else if (source.matches("^\\d+$")) {
Long lDate = Long.valueOf(source);
return new Date(lDate);
}
} catch (Exception e) {
throw new RuntimeException(String.format("parser %s to Date fail", source));
}
throw new RuntimeException(String.format("parser %s to Date fail", source));
}
}
package com.onsiteservice.common.log;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author 潘维吉
* @date 2018-11-07
* 核心统一日志记录切面
* <p>
* 日志AOP切面编程解耦
* 模块以水平切割的方式进行分离统一处理 常用于日志、权限控制、异常处理等业务中
* <p>
* 编程范式主要以下几大类
* AOP(Aspect Oriented Programming)面向切面编程
* OOP(Object Oriented Programming)面向对象编程
* POP(procedure oriented programming)面向过程编程
* FP(Functional Programming)面向函数编程
* <p>
* AOP注解
* @Aspect: 切面,由通知和切入点共同组成,这个注解标注在类上表示为一个切面。
* @Joinpoint: 连接点,被AOP拦截的类或者方法,在前置通知中有介绍使用@Joinpoint获取类名、方法、请求参数。
* Advice: 通知的几种类型
* @Before: 前置通知,在某切入点@Pointcut之前的通知
* @After: 后置通知,在某切入点@Pointcut之后的通知无论成功或者异常。
* @AfterReturning: 返回后通知,方法执行return之后,可以对返回的数据做加工处理。
* @Around: 环绕通知,在方法的调用前、后执行。
* @AfterThrowing: 抛出异常通知,程序出错跑出异常会执行该通知方法。
* @Pointcut: 切入点,从哪里开始。例如从某个包开始或者某个包下的某个类等。
* <p>
* @Profile实现多环境下的配置切换 @Profile("dev")表明只有为dev时才会实例化此类 @Profile支持类和方法 支持多参数@Profile({"dev","test"})
*/
@ConditionalOnProperty(prefix = "project.log", name = {"enabled"}, matchIfMissing = false)
@Profile("!prod")
@Component
@Aspect
@Slf4j
public class LogAspect {
/**
* 定义一个公共的方法,实现切入点
* 拦截Controller下面的所有方法 任何参数(..表示拦截任何参数)
* 以@RestController注解作为切入点 可切入其他业务模块的方法
*
* @within和@target针对类的注解,
* @annotation是针对方法的注解,为自定义注解
*/
//@Pointcut("execution(public * com.*.controller..*.*(..))")
@Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
public void log() {
//方法为空,因为这只是一个切入点,实现在切面编程
}
/**
* 拦截方法之前的一段业务逻辑
*
* @param joinPoint
*/
@Before("log()")
public void doBefore(JoinPoint joinPoint) {
HttpServletRequest request = getHttpServletRequest();
Map params = new LinkedHashMap(10);
params.put("url", request.getRequestURL()); // 获取请求的url
params.put("method", request.getMethod()); // 获取请求的方式
params.put("args", joinPoint.getArgs()); // 请求参数
params.put("className", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); // 获取类名和方法名
params.put("ip", request.getRemoteAddr()); // 获取请求的ip地址
// 输出格式化后的json字符串 IgnoreErrorGetter后FastJson会忽略有问题的get异常解析 如文件流数据 返回其它正常的解析数据
log.info("\n\n[ {} ]接口 日志请求数据: \n{}\n", request.getRequestURI(),
JSON.toJSONString(params, SerializerFeature.IgnoreErrorGetter));
}
/**
* 环绕通知 在方法的调用前、后执行
*/
@Around("log()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = getHttpServletRequest();
//开始时间
long begin = System.currentTimeMillis();
//方法环绕proceed结果
Object obj = point.proceed();
//结束时间
long end = System.currentTimeMillis();
//时间差
long timeDiff = (end - begin);
String msg = "\n\n[ " + request.getRequestURI() + " ]接口 方法性能分析: \n执行总耗时 {}毫秒 来自Dream的表情: ";
int time = 200;
if (timeDiff < time) {
log.info(msg + "高兴\uD83D\uDE00 \n", timeDiff);
} else {
log.warn(msg + "惊讶\uD83D\uDE31 总耗时超过" + time + "ms记得优化哦 \n", timeDiff);
}
return obj;
}
/**
* 获取响应返回值 方法执行return之后
*
* @param object
*/
@AfterReturning(returning = "object", pointcut = "log()")
public void doAfterReturning(Object object) {
HttpServletRequest request = getHttpServletRequest();
// 会打印出一个对象,想打印出具体内容需要在定义模型处加上toString()
log.info("\n\n[ {} ]接口 日志响应结果: \n{}\n", request.getRequestURI(), object.toString());
}
/**
* 拦截方法之后的一段业务逻辑
*/
/*
@After("log()")
public void doAfter() {
log.info("doAfter");
}
*/
/**
* 获取HttpServletRequest
*/
private HttpServletRequest getHttpServletRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes.getRequest();
}
}
package com.onsiteservice.common.log;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.eventbus.Subscribe;
import com.onsiteservice.constant.constant.Constants;
import com.onsiteservice.core.exception.ServiceException;
import com.onsiteservice.core.result.Result;
import com.onsiteservice.dao.mapper.base.BaseErrorLogMapper;
import com.onsiteservice.entity.base.BaseErrorLog;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import javax.annotation.Resource;
/**
* @author 潘维吉
* @date 2019-10-11
* 定义EventBus事件消费者逻辑
* 利用Guava的注解@Subscribe指定当某个事件发生时执行所制定的逻辑
*/
@Component
public class LogEventBusListener {
@Value("${spring.profiles.active}")
private String env;
@Resource
private NativeWebRequest nativeWebRequest;
@Resource
private BaseErrorLogMapper baseErrorLogMapper;
/**
* 当所注册到的事件总线post上发生Result消息时被订阅执行
* MvcConfig 统一异常执行 事件发送 eventBus.post(result)
* 要用于异常日志持久化存储查询
*/
@Subscribe
public void listener(Result result) {
BaseErrorLog log = new BaseErrorLog();
//当前用户Id
Integer userId = (Integer) nativeWebRequest.getAttribute(Constants.CURRENT_USER_ID, RequestAttributes.SCOPE_REQUEST);
//其他json信息
JSONObject data = ((JSONObject) JSON.toJSON(result.getData()));
Integer code = result.getCode();
String content = result.getMsg();
log.setProjectName(data.getString("projectName"));
log.setEnvironment(this.env);
log.setErrorCode(String.valueOf(code));
log.setContent(content);
log.setPlatform(System.getProperty("os.name"));
log.setCreateBy(userId == null ? null : Long.valueOf(userId));
try {
baseErrorLogMapper.insertSelective(log);
} catch (Exception e) {
throw new ServiceException("异常日志保存失败:" + result.getMsg());
}
}
}
package com.onsiteservice.common.log;
import java.lang.annotation.*;
/**
* @author 潘维吉
* @date 2019-08-14 9:24
* 自定义注解 自定义AOP日志记录
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLog {
/**
* 日志描述
*/
String description() default "";
}
package com.onsiteservice.common.log;
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.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @author 潘维吉
* @date 2019-08-14 9:33
* 自定义ServiceLog注解 日志记录切面
*/
@ConditionalOnProperty(prefix = "project.service-log", name = {"enabled"}, matchIfMissing = false)
@Aspect
@Component
@Slf4j
public class ServiceLogAspect {
/**
* 环绕通知方法
*
* @param joinPoint 连接点
* @return 切入点返回值
* @throws Throwable 异常信息
*/
@Around("@within(com.monorepo.common.log.ServiceLog) ||@annotation(com.monorepo.common.log.ServiceLog)")
public Object doServiceLog(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 拦截的实体类
Object target = joinPoint.getTarget();
// 拦截的方法名称
String methodName = signature.getName();
// 拦截的放参数类型
Class[] parameterTypes = signature.getMethod().getParameterTypes();
Method method = target.getClass().getMethod(methodName, parameterTypes);
if (null == method) {
return joinPoint.proceed();
}
// 判断是否包含自定义的注解
if (!method.isAnnotationPresent(ServiceLog.class)) {
return joinPoint.proceed();
}
String logDescription = getServiceMethodDescription(joinPoint);
String methodParams = ServiceLogUtils.getMethodParams(joinPoint);
log.info(logDescription);
log.info("开始请求: 方法:{}, 参数:{}", methodName, methodParams);
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
String deleteSensitiveContent = ServiceLogUtils.deleteSensitiveContent(result);
log.info("结束请求: 耗时:{}毫秒, 返回结果:{}", end - start, deleteSensitiveContent);
return result;
}
/**
* 获取注解中对方法的描述信息 用于service层注解
*
* @param joinPoint
* @return
*/
private String getServiceMethodDescription(ProceedingJoinPoint joinPoint) {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = null;
try {
targetClass = Class.forName(targetName);
} catch (ClassNotFoundException e) {
throw new ClassCastException();
}
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length && null != method.getAnnotation(ServiceLog.class)) {
description = method.getAnnotation(ServiceLog.class).description();
break;
}
}
}
return description;
}
}
package com.onsiteservice.common.log;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* @author 潘维吉
* @date 2019-08-14 9:40
* AOP记录日志的一些共用方法
*/
public class ServiceLogUtils {
private ServiceLogUtils() {
}
/**
* 获取需要记录日志方法的参数,敏感参数用*代替
*
* @param joinPoint 切点
* @return 去除敏感参数后的Json字符串
*/
public static String getMethodParams(ProceedingJoinPoint joinPoint) {
Object[] arguments = joinPoint.getArgs();
StringBuilder sb = new StringBuilder();
if (arguments == null || arguments.length <= 0) {
return sb.toString();
}
for (Object arg : arguments) {
//移除敏感内容
String paramStr;
if (arg instanceof HttpServletResponse) {
paramStr = HttpServletResponse.class.getSimpleName();
} else if (arg instanceof HttpServletRequest) {
paramStr = HttpServletRequest.class.getSimpleName();
} else if (arg instanceof MultipartFile) {
long size = ((MultipartFile) arg).getSize();
paramStr = MultipartFile.class.getSimpleName() + " size:" + size;
} else {
paramStr = deleteSensitiveContent(arg);
}
sb.append(paramStr).append(",");
}
return sb.deleteCharAt(sb.length() - 1).toString();
}
/**
* 删除参数中的敏感内容
*
* @param obj 参数对象
* @return 去除敏感内容后的参数对象
*/
public static String deleteSensitiveContent(Object obj) {
JSONObject jsonObject = new JSONObject();
if (obj == null || obj instanceof Exception) {
return jsonObject.toJSONString();
}
String param = JSON.toJSONString(obj);
try {
jsonObject = JSONObject.parseObject(param);
} catch (Exception e) {
return String.valueOf(obj);
}
List<String> sensitiveFieldList = getSensitiveFieldList();
for (String sensitiveField : sensitiveFieldList) {
if (jsonObject.containsKey(sensitiveField)) {
jsonObject.put(sensitiveField, "******");
}
}
return jsonObject.toJSONString();
}
/**
* 敏感字段列表(当然这里你可以更改为可配置的)
*/
private static List<String> getSensitiveFieldList() {
List<String> sensitiveFieldList = Lists.newArrayList();
sensitiveFieldList.add("pwd");
sensitiveFieldList.add("password");
return sensitiveFieldList;
}
}
package com.onsiteservice.common.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
/**
* @author 潘维吉
* @date 2019/12/5 9:23
* @email 406798106@qq.com
* @description 统一添加Redis key前缀 注入到RedisConfig中setKeySerializer
*/
//@Component //需要时开启
@Slf4j
public class KeyStringRedisSerializer implements RedisSerializer<String> {
private final Charset charset;
@Value("${spring.cache.redis.key-prefix:''}")
private String keyPrefix;
public KeyStringRedisSerializer() {
this(Charset.forName("UTF8"));
}
public KeyStringRedisSerializer(Charset charset) {
Assert.notNull(charset, "Charset must not be null!");
this.charset = charset;
}
@Override
public String deserialize(byte[] bytes) {
String saveKey = new String(bytes, charset);
int indexOf = saveKey.indexOf(keyPrefix);
if (indexOf > 0) {
log.info("key缺少前缀");
} else {
saveKey = saveKey.substring(indexOf);
}
log.info("saveKey:{}", saveKey);
return (saveKey.getBytes() == null ? null : saveKey);
}
@Override
public byte[] serialize(String string) {
String key = keyPrefix + string;
log.info("key:{},getBytes:{}", key, key.getBytes(charset));
return (key == null ? null : key.getBytes(charset));
}
}
package com.onsiteservice.common.redis;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
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.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* @author 潘维吉
* @date 2018-05-21
* Redis缓存配置类
*/
@ConditionalOnProperty(prefix = "project.redis", name = {"enabled"}, matchIfMissing = true)
@Configuration
@EnableCaching
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
/**
* 注解方式的缓存的过期时间
*/
@Value("${spring.cache.redis.time-to-live:1d}")
private Duration timeToLive;
/**
* 定义 RedisTemplate ,指定序列化和反序列化的处理类
* 使用普通的字符串保存 默认序列化的是不可读的复杂的字符串
*
* @param redisConnectionFactory
* @return RedisTemplate
* @ConditionOnMissingBean 如果已经创建了bean,则相关的初始化代码不再执行
*/
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 使用fastJson序列化
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
/**
* 定义 StringRedisTemplate ,指定序列化和反序列化的处理类
* 使用普通的字符串保存 默认序列化的是不可读的复杂的字符串
*
* @param redisConnectionFactory
* @return StringRedisTemplate
* @ConditionOnMissingBean 如果已经创建了bean,则相关的初始化代码不再执行
*/
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
/**
* RedisCache配置 配置key过期时间
*
* @param connectionFactory
* @return
*/
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(timeToLive) // 默认缓存时间 可根据key自定义过期时间
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(initialCacheConfigurations(config))
.transactionAware()
.build();
return redisCacheManager;
}
/**
* RedisCache配置 配置key过期时间等
*/
private Map<String, RedisCacheConfiguration> initialCacheConfigurations(RedisCacheConfiguration config) {
return new HashMap<>() {{
// 根据业务key配置
// put("key", config.entryTtl(Duration.ofDays(30)));
}};
}
}
package com.onsiteservice.common.redis;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author 潘维吉
* @date 2018-05-28
* Redis操作工具类 封装
* 可使用redisTemplate.executePipelined()流水线方式提高读写性能
* 可开启Redis NoSQL事务功能
*/
@Service
@SuppressWarnings("unchecked")
public class RedisUtils {
@Resource
private RedisTemplate redisTemplate;
/**
* 写入缓存
*/
public boolean set(final 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;
}
/**
* 读取缓存
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 写入缓存设置时效时间 支持list类型等
*/
public boolean set(final String key, Object value, Long expireTime, TimeUnit timeUnit) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, timeUnit);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 删除对应的key
*
* @param key
*/
public boolean remove(final String key) {
if (exists(key)) {
return redisTemplate.delete(key);
} else {
return false;
}
}
/**
* 批量删除对应的key
*/
public void remove(final String... keys) {
for (String key : keys) {
remove(key);
}
}
/**
* 正则批量删除key
*/
public void removePattern(final String pattern) {
Set<Serializable> keys = redisTemplate.keys(pattern);
if (keys.size() > 0) {
redisTemplate.delete(keys);
}
}
/**
* 判断缓存中是否有对应的value
*
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 列表添加
*/
public void addList(String key, Object value) {
ListOperations<String, Object> list = redisTemplate.opsForList();
list.rightPush(key, value);
}
/**
* 列表全部添加
*/
public void addAllList(String key, Object value) {
remove(key); // 全部添加List 先清空
ListOperations<String, Object> list = redisTemplate.opsForList();
list.rightPushAll(key, value);
}
/**
* 列表获取
*/
public <T> List<T> getList(String key, long length, long length1) {
ListOperations<String, Object> list = redisTemplate.opsForList();
return (List<T>) list.range(key, length, length1);
}
/**
* 获取列表全部
*/
public <T> List<T> getAllList(String key) {
ListOperations<String, Object> list = redisTemplate.opsForList();
return (List<T>) list.range(key, 0, list.size(key) - 1);
}
/**
* Set集合添加
*/
public void addSet(String key, Object value) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
set.add(key, value);
}
/**
* Set集合获取
*/
public Set<Object> getSet(String key) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
return set.members(key);
}
/**
* 删除Set集合数据
*
* @param key 为Set集合的key
* @param values 为Set集合的多条具体数据value值
*/
public void removeSet(String key, Object... values) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
set.remove(key, values);
}
/**
* 有序集合添加
*/
public void addZSet(String key, Object value, double resources) {
ZSetOperations<String, Object> zSet = redisTemplate.opsForZSet();
zSet.add(key, value, resources);
}
/**
* 有序集合获取
*/
public Set<Object> getZSet(String key, double resources, double resources1) {
ZSetOperations<String, Object> zSet = redisTemplate.opsForZSet();
return zSet.rangeByScore(key, resources, resources1);
}
/**
* 哈希 添加
*/
public void setHash(String key, Object hashKey, Object value) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
hash.put(key, hashKey, value);
}
/**
* 哈希获取数据
*/
public Object getHash(String key, Object hashKey) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
return hash.get(key, hashKey);
}
/**
* 重置失效时间
*/
public void expire(final String key, Long expireTime, TimeUnit timeUnit) {
redisTemplate.expire(key, expireTime, timeUnit);
}
}
package com.onsiteservice.common.runner;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
/**
* @author 潘维吉
* @date 2019-10-14 14:10
* Spring Boot启动执行
* 该接口的方法会在服务启动之后被立即执行
* 主要用来做一些初始化的工作
* 该方法的运行是在SpringApplication.run() 执行完毕之前执行
*/
@Component
@Order(1)
@Slf4j
public class CommonRunner implements CommandLineRunner {
@Value("${spring.profiles.active:dev}")
private String env;
/**
* 会在服务启动完成后立即执行
*/
@Override
public void run(String... args) {
if ("dev".equals(env)) {
// Git提交信息规范强制限制
String projectPath = System.getProperty("user.dir") + "\\";
String fileName = "prepare-commit-msg";
// FileUtils.deleteQuietly(new File(projectPath + ".git\\hooks\\"+fileName));
try {
FileUtils.copyFile(
new File(projectPath + fileName),
new File(projectPath + ".git\\hooks\\" + fileName));
} catch (IOException e) {
// e.printStackTrace();
}
}
}
}
package com.onsiteservice.common.runner;
import com.onsiteservice.core.exception.ServiceException;
import com.onsiteservice.dao.mapper.sys.SysDictMapper;
import com.onsiteservice.entity.sys.SysDict;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Springboot启动工程初始化数据到内存全局缓存中配置类
* 如 字典表数据(使用注解翻译字典值)
*/
@Order(2)
@ConditionalOnProperty(prefix = "project.init-data", name = {"enabled"}, matchIfMissing = true)
@Component
@Slf4j
public class InitDataRunner implements CommandLineRunner {
public static Map<String, List<Map>> dictData = new ConcurrentHashMap<>();
@Resource
private SysDictMapper sysDictMapper;
@Override
public void run(String... args) {
// log.info("InitDataConfig初始化字典表数据到内存中");
try {
Thread.sleep(500);
List<SysDict> list = sysDictMapper.selectAll().stream()
.filter(item -> item.getIsEnabled())
.sorted(Comparator.comparing(SysDict::getShowOrder))
.collect(Collectors.toList());
if (list != null && list.size() > 0) {
list.forEach(item -> {
if (dictData.containsKey(item.getTypeCode())) {
ArrayList newList = new ArrayList<>(dictData.get(item.getTypeCode()));
newList.add(Map.of("code", item.getCode(), "value", item.getValue()));
dictData.put(item.getTypeCode(), newList);
} else {
dictData.put(item.getTypeCode(), List.of(Map.of("code", item.getCode(), "value", item.getValue())));
}
});
}
} catch (Exception e) {
log.error("InitDataConfig初始化数据异常");
throw new ServiceException("字典数据不存在");
}
}
}
package com.onsiteservice.common.socket;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import javax.annotation.Resource;
/**
* @author 潘维吉
* @date 2018-07-05
* WebSocket配置类
*/
//注解@ConditionalOnProperty能够控制某个configuration是否生效
@ConditionalOnProperty(prefix = "project.websocket", name = {"enabled"}, matchIfMissing = false)
@Configuration
@EnableWebSocket
//@EnableWebSocketMessageBroker //开启STOMP协议 控制器支持@MessageMapping注解接收数据 就像使用@RequestMapping一样
public class SocketConfig implements WebSocketConfigurer {
@Resource
private SocketHandler webSocketHandler;
@Resource
private SocketInterceptor webSocketInterceptor;
/**
* 实现 WebSocketConfigurer 接口,重写 registerWebSocketHandlers 方法,
* 核心实现方法,配置 websocket 入口,允许访问的域、注册 Handler、SockJs 支持和拦截器
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//客户端与服务器端建立连接的点 允许跨域 是否允许使用SockJS协议
// registry.addHandler()注册和路由的功能,客户端发起 websocket 连接,把 /path 交给对应的 handler 处理,而不实现具体的业务逻辑,可以理解为收集和任务分发中心。
registry.addHandler(webSocketHandler, "/websocket")
//addInterceptors为 handler 添加拦截器,可以在调用 handler 前后加入我们自己的逻辑代码
.addInterceptors(webSocketInterceptor)
// setAllowedOrigins(String[] domains),允许指定的域名或 IP (含端口号)建立长连接,不限时使用”*”号,如果指定了域名,则必须要以 http 或 https 开头
.setAllowedOrigins("*");
// 当浏览器不支持WebSocket,使用SockJs支持
registry.addHandler(webSocketHandler, "/sockjs-websocket")
.addInterceptors(webSocketInterceptor)
.setAllowedOrigins("*")
.withSockJS();
}
/* @Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192*4);
container.setMaxBinaryMessageBufferSize(8192*4);
return container;
}*/
}
package com.onsiteservice.common.socket;
import com.alibaba.fastjson.JSON;
import lombok.*;
import org.springframework.web.socket.TextMessage;
/**
* @author 潘维吉
* @date 2018-08-27
* WebSocket 消息返回结果集封装
*/
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class SocketData<T> {
private String key; //消息关键字key
private T data; //消息的内容
private String msg = "WebSocket服务端推送的消息"; //消息的说明
public SocketData(String key, T data) {
this.key = key;
this.data = data;
}
public TextMessage toTextMessage(String key, T data) {
return new TextMessage(JSON.toJSONString(new SocketData(key, data)));
}
public TextMessage toTextMessage(String key, T data, String msg) {
return new TextMessage(JSON.toJSONString(new SocketData(key, data, msg)));
}
}
package com.onsiteservice.common.socket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.onsiteservice.constant.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* @author 潘维吉
* @date 2019-04-09 11:05
* 统一处理 WebSocket消息
*/
@Component
@Slf4j
public class SocketHandler implements WebSocketHandler {
/** 在线用户,将其保存在set中,避免用户重复登录,出现多个session */
private static final Map<String, WebSocketSession> USERS;
private static final String SEND_ALL = "all";
static {
USERS = Collections.synchronizedMap(new HashMap<>());
}
/**
* 给某个用户发送消息
*
* @param userId 用户id
* @param message 消息
*/
public void sendMessageToUser(String userId, TextMessage message) {
WebSocketSession user = USERS.get(userId);
try {
if (user.isOpen()) {
user.sendMessage(message); }
} catch (Exception e) {
log.warn("给userId=" + userId + "用户发送消息异常:" + e.getMessage());
}
}
/**
* 给某些用户发送消息
*
* @param userId 用户id
* @param message 消息
*/
public void sendMessageToSomeUser(TextMessage message, String... userId) {
Arrays.asList(userId).forEach(item -> sendMessageToUser(item, message));
}
/**
* 给所有在线用户发送消息
*
* @param message 文本消息
*/
public void sendMessageToAll(TextMessage message) {
WebSocketSession user = null;
for (String key : USERS.keySet()) {
user = USERS.get(key);
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (Exception e) {
log.warn("给所有在线用户发送消息异常:" + e.getMessage());
}
}
}
/**
* 成功连接WebSocket后执行
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) {
try {
log.info("WebSocket链接成功 \uD83D\uDE02 ");
String userId = (String) session.getAttributes().get(Constants.WEBSOCKET_USER_ID);
if (userId != null) {
USERS.put(userId, session);
// log.info("有新WebSocket连接加入,当前在线人数为:" + USERS.size());
session.sendMessage(new SocketData().toTextMessage("open", "WebSocket连接成功"));
}
} catch (Exception e) {
log.error("WebSocket连接 afterConnectionEstablished异常:" + e.getMessage());
}
}
/**
* 处理收到客户端消息
*
* @param message 客户端发送过来的消息
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
var payload = message.getPayload();
if (payload instanceof String) {
log.info("处理客户端发送的字符串消息:" + payload.toString());
} else if (payload instanceof Object) {
JSONObject msg = JSON.parseObject(payload.toString());
log.info("处理客户端发送的对象消息:" + payload.toString());
JSONObject obj = new JSONObject();
String type = msg.get("type").toString();
if (StringUtils.isNotBlank(type) && SEND_ALL.equals(type)) {
//给所有人
obj.put("msg", msg.getString("msg"));
log.info("给所有人发消息");
sendMessageToAll(new TextMessage(obj.toJSONString()));
} else {
//给个人
String to = msg.getString("to");
obj.put("msg", msg.getString("msg"));
log.info("给个人发消息");
sendMessageToUser(to, new TextMessage(obj.toJSONString()));
}
}
}
/**
* 处理传输错误
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if (session.isOpen()) {
session.close();
}
log.info("链接出错,关闭链接,异常信息:" + exception.getMessage());
String userId = getUserId(session);
if (USERS.get(userId) != null) {
USERS.remove(userId);
}
}
/**
* 在两端WebSocket connection都关闭或transport error发生后执行
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("链接关闭,关闭信息:" + closeStatus.toString());
String userId = getUserId(session);
if (USERS.get(userId) != null) {
USERS.remove(userId);
}
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 获取用户id
*/
private String getUserId(WebSocketSession session) {
try {
return (String) session.getAttributes().get(Constants.WEBSOCKET_USER_ID);
} catch (Exception e) {
return null;
}
}
/**
* 根据当前用户id登出
*
* @param userId 用户id
*/
public void logout(String userId) {
USERS.remove(userId);
log.info("用户登出,userId:" + userId);
}
}
package com.onsiteservice.common.socket;
import com.onsiteservice.constant.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
/**
* @author 潘维吉
* @date 2019-01-19 16:21
* Websocket的握手请求拦截器 可JWT token鉴权
*/
@Component
@Slf4j
public class SocketInterceptor implements HandshakeInterceptor {
/**
* 握手之前执行该方法, 继续握手返回true, 中断握手返回false. 通过attributes参数设置WebSocketSession的属性
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
// log.info("Http协议转换WebSocket协议进行前, 握手前" + request.getURI());
// http协议转换WebSocket协议进行前,这里可通过token信息判断用户是否合法
if (request instanceof ServletServerHttpRequest) {
// HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
String userId = request.getURI().toString().split("userId=")[1];
if (StringUtils.isBlank(userId)) {
log.error("Websocket的握手请求拦截器: 用户id空 无效请求!");
return false;
} else {
log.info("Websocket的握手请求拦截器: 当前session的userId=" + userId);
// 存入session中获取到当前登录的用户信息
attributes.put(Constants.WEBSOCKET_USER_ID, userId);
return true;
}
}
return true;
}
/**
* 在握手之后执行该方法. 无论是否握手成功都指明了响应状态码和相应头
*/
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception ex) {
//握手成功后
// log.info(this.getClass().getCanonicalName() + "握手成功后...");
}
}
package com.onsiteservice.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* @author 潘维吉
* @date 2020/8/12 11:24
* @email 406798106@qq.com
* @description 精确的加减乘除运算工具类
*/
public class ArithUtils {
/**
* 默认除法运算精度
*/
private static final int DEF_DIV_SCALE = 10;
/**
* 提供精确的加法运算
*/
public static double add(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.add(b2).doubleValue();
}
/**
* 精确的减法运算
*/
public static double subtract(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.subtract(b2).doubleValue();
}
/**
* 精确的乘法运算
*/
public static double multiply(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时
* 精确到小数点后10位的数字四舍五入
*/
public static double divide(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, DEF_DIV_SCALE, RoundingMode.HALF_UP).doubleValue();
}
public static void main(String[] args) {
System.out.println("0.05 + 0.01 = " + ArithUtils.add(0.05, 0.01));
System.out.println("1.0 - 0.42 = " + ArithUtils.subtract(1.0, 0.42));
System.out.println("4.015*100 = " + ArithUtils.multiply(4.015, 100));
System.out.println("123.3/100 = " + ArithUtils.divide(123.3, 100));
}
}
package com.onsiteservice.util;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* @author 潘维吉
* @date 2019/11/21 16:15
* @email 406798106@qq.com
* @description 判断包装类数组/集合为空的工具类
*/
public class ArrayUtils {
/**
* 判断集合是否为空
*/
public static boolean isEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}
/**
* 判断List是否为空
*/
public static boolean isEmpty(List<Object> list) {
return list == null || list.size() == 0;
}
/**
* 判断List非空
*/
public static boolean isNotEmpty(List list) {
return list != null && list.size() > 0;
}
/**
* 判断Map是否为空
*/
public static boolean isEmpty(Map<?, ?> map) {
return map == null || map.isEmpty();
}
/**
* 判断数组是否为空
*/
public static boolean isEmpty(Object[] array) {
return array == null || array.length == 0;
}
}
package com.onsiteservice.util;
public class CRC16Util {
/**
* CRC16-Modbus
* @param data
* @return
*/
public static String getCRC16Modbus(String data) {
String result = getCRC16(data);
// 交换高低位,低位在前高位在后
return result.substring(2, 4) + result.substring(0, 2);
}
/**
* CRC16校验码
* @param data
* @return
*/
public static String getCRC16(String data) {
data = data.replace(" ", "");
int len = data.length();
if (!(len % 2 == 0)) {
return "0000";
}
int num = len / 2;
byte[] para = new byte[num];
for (int i = 0; i < num; i++) {
int value = Integer.valueOf(data.substring(i * 2, 2 * (i + 1)), 16);
para[i] = (byte) value;
}
return getCRC16(para);
}
/**
* 计算CRC16校验码
* @param bytes 字节数组
* @return 校验码
*/
public static String getCRC16(byte[] bytes) {
// CRC寄存器全为1
int CRC = 0x0000ffff;
// 多项式校验值
int POLYNOMIAL = 0x0000a001;
int i, j;
for (i = 0; i < bytes.length; i++) {
CRC ^= ((int) bytes[i] & 0x000000ff);
for (j = 0; j < 8; j++) {
if ((CRC & 0x00000001) != 0) {
CRC >>= 1;
CRC ^= POLYNOMIAL;
} else {
CRC >>= 1;
}
}
}
// 结果转换为16进制
String result = Integer.toHexString(CRC).toUpperCase();
if (result.length() != 4) {
StringBuffer sb = new StringBuffer("0000");
result = sb.replace(4 - result.length(), 4, result).toString();
}
return result;
}
}
This diff is collapsed.
package com.onsiteservice.util;
/**
* @author 潘维吉
* @date 2019-12-13 15:08
* @email 406798106@qq.com
* @description 百度坐标(BD09)、国测局坐标(火星坐标,GCJ02(高德、腾讯地图使用))、和WGS84(GPS)坐标系之间的转换的工具
*/
public class CoordinateUtils {
static double x_pi = 3.14159265358979324 * 3000.0 / 180.0;
// π
static double pi = 3.1415926535897932384626;
// 长半轴
static double a = 6378245.0;
// 扁率
static double ee = 0.00669342162296594323;
/**
* WGS84转GCJ02(火星坐标系) GPS坐标转高德和腾讯地图坐标系
*
* @param lng WGS84坐标系的经度
* @param lat WGS84坐标系的纬度
* @return 火星坐标数组
*/
public static double[] wgs84togcj02(double lng, double lat) {
if (outOfChina(lng, lat)) {
return new double[]{lng, lat};
}
double dlat = transformlat(lng - 105.0, lat - 35.0);
double dlng = transformlng(lng - 105.0, lat - 35.0);
double radlat = lat / 180.0 * pi;
double magic = Math.sin(radlat);
magic = 1 - ee * magic * magic;
double sqrtmagic = Math.sqrt(magic);
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi);
dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * pi);
double mglat = lat + dlat;
double mglng = lng + dlng;
return new double[]{mglng, mglat};
}
/**
* GCJ02(火星坐标系)转GPS84
*
* @param lng 火星坐标系的经度
* @param lat 火星坐标系纬度
* @return WGS84坐标数组
*/
public static double[] gcj02towgs84(double lng, double lat) {
if (outOfChina(lng, lat)) {
return new double[]{lng, lat};
}
double dlat = transformlat(lng - 105.0, lat - 35.0);
double dlng = transformlng(lng - 105.0, lat - 35.0);
double radlat = lat / 180.0 * pi;
double magic = Math.sin(radlat);
magic = 1 - ee * magic * magic;
double sqrtmagic = Math.sqrt(magic);
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi);
dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * pi);
double mglat = lat + dlat;
double mglng = lng + dlng;
return new double[]{lng * 2 - mglng, lat * 2 - mglat};
}
/**
* 百度坐标系(BD-09)转WGS坐标
*
* @param lng 百度坐标纬度
* @param lat 百度坐标经度
* @return WGS84坐标数组
*/
public static double[] bd09towgs84(double lng, double lat) {
double[] gcj = bd09togcj02(lng, lat);
double[] wgs84 = gcj02towgs84(gcj[0], gcj[1]);
return wgs84;
}
/**
* WGS坐标转百度坐标系(BD-09)
*
* @param lng WGS84坐标系的经度
* @param lat WGS84坐标系的纬度
* @return 百度坐标数组
*/
public static double[] wgs84tobd09(double lng, double lat) {
double[] gcj = wgs84togcj02(lng, lat);
double[] bd09 = gcj02tobd09(gcj[0], gcj[1]);
return bd09;
}
/**
* 火星坐标系(GCJ-02)转百度坐标系(BD-09)
*
* 谷歌、高德——>百度
*
* @param lng 火星坐标经度
* @param lat 火星坐标纬度
* @return 百度坐标数组
*/
public static double[] gcj02tobd09(double lng, double lat) {
double z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * x_pi);
double theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * x_pi);
double bd_lng = z * Math.cos(theta) + 0.0065;
double bd_lat = z * Math.sin(theta) + 0.006;
return new double[]{bd_lng, bd_lat};
}
/**
* 百度坐标系(BD-09)转火星坐标系(GCJ-02)
*
* 百度——>谷歌、高德
*
* @param bd_lon 百度坐标纬度
* @param bd_lat 百度坐标经度
* @return 火星坐标数组
*/
public static double[] bd09togcj02(double bd_lon, double bd_lat) {
double x = bd_lon - 0.0065;
double y = bd_lat - 0.006;
double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi);
double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi);
double gg_lng = z * Math.cos(theta);
double gg_lat = z * Math.sin(theta);
return new double[]{gg_lng, gg_lat};
}
/**
* 纬度转换
*
* @param lng
* @param lat
* @return
*/
public static double transformlat(double lng, double lat) {
double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
ret += (20.0 * Math.sin(6.0 * lng * pi) + 20.0 * Math.sin(2.0 * lng * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lat * pi) + 40.0 * Math.sin(lat / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(lat / 12.0 * pi) + 320 * Math.sin(lat * pi / 30.0)) * 2.0 / 3.0;
return ret;
}
/**
* 经度转换
*
* @param lng
* @param lat
* @return
*/
public static double transformlng(double lng, double lat) {
double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
ret += (20.0 * Math.sin(6.0 * lng * pi) + 20.0 * Math.sin(2.0 * lng * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lng * pi) + 40.0 * Math.sin(lng / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(lng / 12.0 * pi) + 300.0 * Math.sin(lng / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}
/**
* 判断是否在国内,不在国内不做偏移
*
* @param lng
* @param lat
* @return
*/
public static boolean outOfChina(double lng, double lat) {
if (lng < 72.004 || lng > 137.8347) {
return true;
} else if (lat < 0.8293 || lat > 55.8271) {
return true;
}
return false;
}
}
This diff is collapsed.
package com.onsiteservice.util;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author 潘维吉
* @date 2019-12-13 15:08
* @email 406798106@qq.com
* @description 地理学相关工具类
*/
public class GeologyUtils {
private static double EARTH_RADIUS = 6378.137;
private static double rad(double d) {
return d * Math.PI / 180.0;
}
/**
* 根据经纬度计算两点之间的距离(米)
*/
public static double getDistance(String lon1, String lat1, String lon2, String lat2) {
double radLat1 = rad(Double.valueOf(lat1));
double radLat2 = rad(Double.valueOf(lat2));
double a = radLat1 - radLat2;
double b = rad(Double.valueOf(lon1)) - rad(Double.valueOf(lon2));
double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2)
+ Math.cos(radLat1) * Math.cos(radLat2)
* Math.pow(Math.sin(b / 2), 2)));
s = s * EARTH_RADIUS;
s = Math.round(s * 10000d) / 10000d;
s = s * 1000;
return s;
}
/**
* 将坐标数组转为wkt格式字符串
*/
private static final String WKT_POLYGON_START = "POLYGON(("; // wkt字符串开头
private static final String WKT_POLYGON_END = "))"; // wkt字符串结尾
public static String getWktFromPoints(List<Map<String, String>> points) {
String pointsStr = points.stream().map(p -> p.get("lon") + " " + p.get("lat")).collect(Collectors.joining(","));
return WKT_POLYGON_START.concat(pointsStr.concat(WKT_POLYGON_END));
}
public static String getWktFromPointsByArray(List<Double[]> points) {
String pointsStr = points.stream().map(p -> p[0] + " " + p[1]).collect(Collectors.joining(","));
return WKT_POLYGON_START.concat(pointsStr.concat(WKT_POLYGON_END));
}
/**
* 将坐标数组转为wkt格式字符串
*/
public static List<Map<String, String>> getPoints(String wkt) {
String pointsStr = wkt.replace(WKT_POLYGON_START, "").replace(WKT_POLYGON_END, "");
String[] points = pointsStr.split(",");
List<Map<String, String>> pointList = new ArrayList<>(points.length);
for (String s : points) {
var lonLat = s.split(" ");
pointList.add(Map.of("lon", lonLat[0], "lat", lonLat[1]));
}
return pointList;
}
public static List<Double[]> getPointsToArray(String wkt) {
String pointsStr = wkt.replace(WKT_POLYGON_START, "").replace(WKT_POLYGON_END, "");
String[] points = pointsStr.split(",");
List<Double[]> pointList = new ArrayList<>(points.length);
for (String s : points) {
var lonLat = s.split(" ");
pointList.add(new Double[]{Double.parseDouble(lonLat[0]), Double.parseDouble(lonLat[1])});
}
return pointList;
}
/**
* 判断点是否在圆内
*/
public static boolean isInCircle(String centerLon, String centerLat, String lon, String lat, Double radius) {
// 计算两点距离
double distance = getDistance(centerLon, centerLat, lon, lat);
return distance <= radius;
}
/**
* 判断点是否在多边形内
*
* @param pointLon
* @param pointLat
* @param pointList
* @return
*/
public static boolean isInPolygon(Double pointLon, Double pointLat, List<Double[]> pointList) {
Point2D.Double point = new Point2D.Double(pointLon, pointLat);
List<Point2D.Double> pts = new ArrayList<>();
double polygonPoint_x = 0.0, polygonPoint_y = 0.0;
for (Double[] pointLocation : pointList) {
polygonPoint_x = pointLocation[0];
polygonPoint_y = pointLocation[1];
Point2D.Double polygonPoint = new Point2D.Double(polygonPoint_x, polygonPoint_y);
pts.add(polygonPoint);
}
int N = pts.size();
boolean boundOrVertex = true;
int intersectCount = 0;//交叉点数量
double precision = 2e-10; //浮点类型计算时候与0比较时候的容差
Point2D.Double p1, p2;//临近顶点
Point2D.Double p = point; //当前点
p1 = pts.get(0);
for (int i = 1; i <= N; ++i) {
if (p.equals(p1)) {
return boundOrVertex;
}
p2 = pts.get(i % N);
if (p.x < Math.min(p1.x, p2.x) || p.x > Math.max(p1.x, p2.x)) {
p1 = p2;
continue;
}
//射线穿过算法
if (p.x > Math.min(p1.x, p2.x) && p.x < Math.max(p1.x, p2.x)) {
if (p.y <= Math.max(p1.y, p2.y)) {
if (p1.x == p2.x && p.y >= Math.min(p1.y, p2.y)) {
return boundOrVertex;
}
if (p1.y == p2.y) {
if (p1.y == p.y) {
return boundOrVertex;
} else {
++intersectCount;
}
} else {
double xinters = (p.x - p1.x) * (p2.y - p1.y) / (p2.x - p1.x) + p1.y;
if (Math.abs(p.y - xinters) < precision) {
return boundOrVertex;
}
if (p.y < xinters) {
++intersectCount;
}
}
}
} else {
if (p.x == p2.x && p.y <= p2.y) {
Point2D.Double p3 = pts.get((i + 1) % N);
if (p.x >= Math.min(p1.x, p3.x) && p.x <= Math.max(p1.x, p3.x)) {
++intersectCount;
} else {
intersectCount += 2;
}
}
}
p1 = p2;
}
if (intersectCount % 2 == 0) { //偶数在多边形外
return false;
} else { //奇数在多边形内
return true;
}
}
public static void main(String[] args) {
List<Double[]> locations = new ArrayList<>();
locations.add(new Double[]{119.531351, 35.43546});
locations.add(new Double[]{119.531876, 35.435435});
locations.add(new Double[]{119.5319, 35.435917});
locations.add(new Double[]{119.532987, 35.435922});
locations.add(new Double[]{119.532594, 35.433276});
locations.add(new Double[]{119.532365, 35.433202});
locations.add(new Double[]{119.531303, 35.433281});
locations.add(new Double[]{119.531303, 35.433281});
locations.add(new Double[]{119.531351, 35.43546});
double[] center = CoordinateUtils.wgs84togcj02(119.526047, 35.435047);
Boolean isIn = isInPolygon(119.526047, 35.435047, locations);
System.out.println(isIn);
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
package com.onsiteservice.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author 潘维吉
* @date 2020/4/21 15:32
* @email 406798106@qq.com
* @description 正则验证和常量工具类
*/
public class RegexUtils {
/**
* 正则统一常量定义
*/
public static final String PHONE = "^1\\d{10}$";
public static final String NUM = "^[0-9]*$";
/**
* 验证字符串是否符合表达式
*
* @param regex 正则表达式
* @param value 待匹配值
* @return
* @throws
* @author liub
*/
public static boolean isMatch(String regex, String value) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(value);
return m.matches();
}
}
This diff is collapsed.
This diff is collapsed.
package com.onsiteservice.util.command;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
/**
* @author 潘维吉
* @date 2019-03-06 9:25
* 执行shell/cmd命令工具类封装返回结果 提供了超时功能
*/
@Data
@AllArgsConstructor
@ToString
public class CommandResult {
private int exitCode;
private String executeOut;
}
This diff is collapsed.
package com.onsiteservice.constant.constant;
/**
* 业务常量
*/
public class BizConstants {
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment