55 changed files with 4111 additions and 399 deletions
-
103admin/pom.xml
-
18admin/src/main/java/com/canvas/web/AdminApplication.java
-
11admin/src/main/java/com/canvas/web/controller/TestController.java
-
126admin/src/main/resources/application-dev.yml
-
27admin/src/main/resources/application.yml
-
51common/pom.xml
-
11common/src/main/java/com/canvas/web/annotation/AnonymousAccess.java
-
59common/src/main/java/com/canvas/web/annotation/Query.java
-
59common/src/main/java/com/canvas/web/annotation/rest/AnonymousGetMapping.java
-
38common/src/main/java/com/canvas/web/base/BaseDTO.java
-
41common/src/main/java/com/canvas/web/config/CommonConfig.java
-
38common/src/main/java/com/canvas/web/config/UploadFileConfig.java
-
19common/src/main/java/com/canvas/web/service/IUplpadService.java
-
30common/src/main/java/com/canvas/web/service/impl/UploadServiceImpl.java
-
39common/src/main/java/com/canvas/web/utils/CacheKey.java
-
13common/src/main/java/com/canvas/web/utils/CallBack.java
-
294common/src/main/java/com/canvas/web/utils/CommonUtils.java
-
26common/src/main/java/com/canvas/web/utils/ElAdminConstant.java
-
42common/src/main/java/com/canvas/web/utils/ErrorCode.java
-
356common/src/main/java/com/canvas/web/utils/FileUtil.java
-
112common/src/main/java/com/canvas/web/utils/JsonResult.java
-
696common/src/main/java/com/canvas/web/utils/RedisUtils.java
-
121common/src/main/java/com/canvas/web/utils/SpringContextHolder.java
-
268common/src/main/java/com/canvas/web/utils/StringUtils.java
-
305common/src/main/java/com/canvas/web/utils/UploadUtils.java
-
287pom.xml
-
58system/pom.xml
-
45system/src/main/java/AppRun.java
-
24system/src/main/java/modules/security/config/ConfigBeanConfiguration.java
-
31system/src/main/java/modules/security/config/SpringSecurityConfig.java
-
48system/src/main/java/modules/security/config/bean/LoginCode.java
-
25system/src/main/java/modules/security/config/bean/LoginCodeEnum.java
-
91system/src/main/java/modules/security/config/bean/LoginProperties.java
-
52system/src/main/java/modules/security/config/bean/SecurityProperties.java
-
19system/src/main/java/modules/security/security/JwtAccessDeniedHandler.java
-
19system/src/main/java/modules/security/security/JwtAuthenticationEntryPoint.java
-
51system/src/main/java/modules/security/security/SecurityProperties.java
-
16system/src/main/java/modules/security/security/TokenConfigurer.java
-
107system/src/main/java/modules/security/security/TokenProvider.java
-
19system/src/main/java/modules/security/service/OnlineUserService.java
-
31system/src/main/java/modules/security/service/UserCacheClean.java
-
74system/src/main/java/modules/security/service/UserDetailsServiceImpl.java
-
22system/src/main/java/modules/security/service/dto/AuthUserDto.java
-
65system/src/main/java/modules/security/service/dto/JwtUserDto.java
-
15system/src/main/java/modules/system/service/DataService.java
-
16system/src/main/java/modules/system/service/RoleService.java
-
98system/src/main/java/modules/system/service/UserService.java
-
76system/src/main/java/modules/system/service/dto/MenuDto.java
-
45system/src/main/java/modules/system/service/dto/RoleDto.java
-
16system/src/main/java/modules/system/service/dto/RoleSmallDto.java
-
50system/src/main/java/modules/system/service/dto/UserDto.java
-
32system/src/main/java/modules/system/service/dto/UserQueryCriteria.java
-
115system/src/main/resources/config/application-dev.yml
-
0system/src/main/resources/config/application-prod.yml
-
60system/src/main/resources/config/application.yml
@ -1,103 +0,0 @@ |
|||
<?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>yxk_canvasScreen</artifactId> |
|||
<groupId>com.canvas.web</groupId> |
|||
<version>1.0-SNAPSHOT</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<artifactId>admin</artifactId> |
|||
<name>启动入口</name> |
|||
|
|||
<properties> |
|||
<maven.compiler.source>17</maven.compiler.source> |
|||
<maven.compiler.target>17</maven.compiler.target> |
|||
</properties> |
|||
|
|||
|
|||
<!-- 依赖声明 --> |
|||
<dependencies> |
|||
<!-- 核心模块 --> |
|||
<dependency> |
|||
<groupId>com.canvas.web</groupId> |
|||
<artifactId>system</artifactId> |
|||
<version>1.0-SNAPSHOT</version> |
|||
</dependency> |
|||
<!-- 代码生成 --> |
|||
<dependency> |
|||
<groupId>com.canvas.web</groupId> |
|||
<artifactId>generator</artifactId> |
|||
<version>1.0-SNAPSHOT</version> |
|||
</dependency> |
|||
<!-- 定时任务 --> |
|||
<dependency> |
|||
<groupId>com.canvas.web</groupId> |
|||
<artifactId>quartz</artifactId> |
|||
<version>1.0-SNAPSHOT</version> |
|||
</dependency> |
|||
<!-- 消息队列 --> |
|||
<dependency> |
|||
<groupId>com.canvas.web</groupId> |
|||
<artifactId>queue</artifactId> |
|||
<version>1.0-SNAPSHOT</version> |
|||
</dependency> |
|||
|
|||
|
|||
|
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.amqp</groupId> |
|||
<artifactId>spring-rabbit-test</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
<!-- 引入阿里数据库连接池 --> |
|||
<dependency> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>druid-spring-boot-starter</artifactId> |
|||
<version>1.2.8</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>log4j</groupId> |
|||
<artifactId>log4j</artifactId> |
|||
<version>1.2.17</version> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<build> |
|||
<pluginManagement> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-maven-plugin</artifactId> |
|||
<version>2.1.11.RELEASE</version> |
|||
<configuration> |
|||
<finalName>JavaWeb_Vue</finalName> |
|||
</configuration> |
|||
<executions> |
|||
<execution> |
|||
<goals> |
|||
<goal>repackage</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
|
|||
</plugins> |
|||
</pluginManagement> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-maven-plugin</artifactId> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</project> |
@ -1,18 +0,0 @@ |
|||
package com.canvas.web; |
|||
|
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
import org.springframework.scheduling.annotation.EnableScheduling; |
|||
import org.springframework.transaction.annotation.EnableTransactionManagement; |
|||
|
|||
@SpringBootApplication(scanBasePackages = {"com.canvas.*"}) |
|||
@EnableTransactionManagement |
|||
@EnableScheduling |
|||
public class AdminApplication { |
|||
|
|||
public static void main(String[] args){ |
|||
SpringApplication.run(AdminApplication.class,args); |
|||
System.out.println("多媒体后台管理系统启动成功!"); |
|||
} |
|||
} |
@ -1,11 +0,0 @@ |
|||
package com.canvas.web.controller; |
|||
|
|||
|
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
//业务测试接口 |
|||
@RestController |
|||
@RequestMapping("/test") |
|||
public class TestController { |
|||
} |
@ -1,126 +0,0 @@ |
|||
#自定义配置 |
|||
canvasweb: |
|||
image-url: https://images.canvase.com/ |
|||
app-debug: true |
|||
|
|||
|
|||
|
|||
spring: |
|||
# 配置数据源 |
|||
datasource: |
|||
# 使用阿里的Druid连接池 |
|||
druid: |
|||
db-type: com.alibaba.druid.pool.DruidDataSource |
|||
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy |
|||
url: jdbc:mysql://192.168.99.207:3306/javaweb.vue?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&useSSL=true&tinyInt1isBit=false |
|||
username: root |
|||
password: ftzn83560792 |
|||
|
|||
|
|||
# 连接池的配置信息 |
|||
# 初始连接数 |
|||
initialSize: 5 |
|||
# 最小连接池数量 |
|||
minIdle: 5 |
|||
# 最大连接池数量 |
|||
maxActive: 20 |
|||
# 配置获取连接等待超时的时间 |
|||
maxWait: 60000 |
|||
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 |
|||
timeBetweenEvictionRunsMillis: 60000 |
|||
# 配置一个连接在池中最小生存的时间,单位是毫秒 |
|||
minEvictableIdleTimeMillis: 300000 |
|||
# 配置一个连接在池中最大生存的时间,单位是毫秒 |
|||
maxEvictableIdleTimeMillis: 900000 |
|||
# 配置检测连接是否有效 |
|||
validationQuery: SELECT 1 FROM DUAL |
|||
testWhileIdle: true |
|||
testOnBorrow: false |
|||
testOnReturn: false |
|||
# 打开PSCache,并且指定每个连接上PSCache的大小 |
|||
poolPreparedStatements: true |
|||
maxPoolPreparedStatementPerConnectionSize: 20 |
|||
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 |
|||
filters: stat,wall,log4j |
|||
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录 |
|||
connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000 |
|||
# 配置DruidStatFilter |
|||
webStatFilter: |
|||
enabled: true |
|||
url-pattern: "/*" |
|||
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*" |
|||
# 配置DruidStatViewServlet |
|||
statViewServlet: |
|||
url-pattern: "/druid/*" |
|||
# IP白名单(没有配置或者为空,则允许所有访问) |
|||
allow: 127.0.0.1,192.168.163.1 |
|||
# IP黑名单 (存在共同时,deny优先于allow) |
|||
deny: 192.168.1.73 |
|||
# 禁用HTML页面上的“Reset All”功能 |
|||
reset-enable: false |
|||
# 登录名 |
|||
login-username: admin |
|||
# 登录密码 |
|||
login-password: 123456 |
|||
|
|||
# Redis数据源 |
|||
redis: |
|||
# 缓存库默认索引0 |
|||
database: 0 |
|||
# Redis服务器地址 |
|||
host: 127.0.0.1 |
|||
# Redis服务器连接端口 |
|||
port: 6379 |
|||
# Redis服务器连接密码(默认为空) |
|||
password: |
|||
# 连接超时时间(毫秒) |
|||
timeout: 6000 |
|||
# 默认的数据过期时间,主要用于shiro权限管理 |
|||
expire: 2592000 |
|||
jedis: |
|||
pool: |
|||
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制) |
|||
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) |
|||
max-idle: 10 # 连接池中的最大空闲连接 |
|||
min-idle: 1 # 连接池中的最小空闲连接 |
|||
|
|||
file: |
|||
#上传的服务器上的映射文件夹 |
|||
accessPath: /uploads/ |
|||
#静态资源对外暴露的访问路径 |
|||
staticAccessPath: /** |
|||
#静态资源实际存储路径 |
|||
uploadFolder: E:\JavaWeb_Vue\JavaWeb\uploads\ |
|||
|
|||
|
|||
# Shiro |
|||
shiro: |
|||
cipher-key: f/SX5TIve5WWzT4aQlABJA== |
|||
cookie-name: shiro-cookie2 |
|||
user: |
|||
# 登录地址 |
|||
loginUrl: /login |
|||
# 权限认证失败地址 |
|||
unauthorizedUrl: /unauth |
|||
# 首页地址 |
|||
indexUrl: /index |
|||
# 验证码开关 |
|||
captchaEnabled: true |
|||
# 验证码类型 math 数组计算 char 字符 |
|||
captchaType: math |
|||
cookie: |
|||
# 设置Cookie的域名 默认空,即当前访问的域名 |
|||
domain: |
|||
# 设置cookie的有效访问路径 |
|||
path: / |
|||
# 设置HttpOnly属性 |
|||
httpOnly: true |
|||
# 设置Cookie的过期时间,天为单位 |
|||
maxAge: 30 |
|||
session: |
|||
# Session超时时间(默认30分钟) |
|||
expireTime: 300 |
|||
# 同步session到数据库的周期(默认1分钟) |
|||
dbSyncPeriod: 1 |
|||
# 相隔多久检查一次session的有效性,默认就是10分钟 |
|||
validationInterval: 10 |
@ -1,27 +0,0 @@ |
|||
server: |
|||
port: 9030 |
|||
servlet: |
|||
context-path: /api |
|||
|
|||
spring: |
|||
profiles: |
|||
active: dev |
|||
|
|||
#配置 Jpa |
|||
jpa: |
|||
properties: |
|||
hibernate: |
|||
ddl-auto: none |
|||
open-in-view: true |
|||
|
|||
|
|||
task: |
|||
pool: |
|||
# 核心线程池大小 |
|||
core-pool-size: 10 |
|||
# 最大线程数 |
|||
max-pool-size: 30 |
|||
# 活跃时间 |
|||
keep-alive-seconds: 60 |
|||
# 队列容量 |
|||
queue-capacity: 50 |
@ -0,0 +1,11 @@ |
|||
package com.canvas.web.annotation; |
|||
|
|||
|
|||
import java.lang.annotation.*; |
|||
|
|||
@Inherited |
|||
@Documented |
|||
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE}) |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
public @interface AnonymousAccess { |
|||
} |
@ -0,0 +1,59 @@ |
|||
package com.canvas.web.annotation; |
|||
|
|||
|
|||
import java.lang.annotation.ElementType; |
|||
import java.lang.annotation.Retention; |
|||
import java.lang.annotation.RetentionPolicy; |
|||
import java.lang.annotation.Target; |
|||
|
|||
@Target(ElementType.FIELD) |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
public @interface Query { |
|||
|
|||
//基本对象的属性名 |
|||
String propName() default ""; |
|||
|
|||
//查询方式 |
|||
Type type() default Type.EQUAL; |
|||
|
|||
//连接查询的属性名,如User类中的dept |
|||
String joinName() default ""; |
|||
|
|||
//默认左连接 |
|||
Join join() default Join.LEFT; |
|||
|
|||
//多字段模糊搜索,仅支持String类型字段,多个用逗号隔开,例如:@Query(blurry="email,username") |
|||
String blurry() default ""; |
|||
|
|||
enum Type { |
|||
// 相等 |
|||
EQUAL |
|||
// 大于等于 |
|||
, GREATER_THAN |
|||
// 小于等于 |
|||
, LESS_THAN |
|||
// 中模糊查询 |
|||
, INNER_LIKE |
|||
// 左模糊查询 |
|||
, LEFT_LIKE |
|||
// 右模糊查询 |
|||
, RIGHT_LIKE |
|||
// 小于 |
|||
, LESS_THAN_NQ |
|||
// 包含 |
|||
, IN |
|||
// 不等于 |
|||
,NOT_EQUAL |
|||
// between |
|||
,BETWEEN |
|||
// 不为空 |
|||
,NOT_NULL |
|||
// 为空 |
|||
,IS_NULL |
|||
} |
|||
//适用于简单连接查询,复杂的查询适用sql或其他注解 |
|||
enum Join { |
|||
|
|||
LEFT, RIGHT, INNER |
|||
} |
|||
} |
@ -0,0 +1,59 @@ |
|||
package com.canvas.web.annotation.rest; |
|||
|
|||
import com.canvas.web.annotation.AnonymousAccess; |
|||
import org.springframework.core.annotation.AliasFor; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestMethod; |
|||
|
|||
import java.lang.annotation.*; |
|||
|
|||
@AnonymousAccess |
|||
@Target(ElementType.METHOD) |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
@Documented |
|||
@RequestMapping(method = RequestMethod.GET) |
|||
public @interface AnonymousGetMapping { |
|||
/** |
|||
* Alias for {@link RequestMapping#name}. |
|||
*/ |
|||
@AliasFor(annotation = RequestMapping.class) |
|||
String name() default ""; |
|||
|
|||
/** |
|||
* Alias for {@link RequestMapping#value}. |
|||
*/ |
|||
@AliasFor(annotation = RequestMapping.class) |
|||
String[] value() default {}; |
|||
|
|||
/** |
|||
* Alias for {@link RequestMapping#path}. |
|||
*/ |
|||
@AliasFor(annotation = RequestMapping.class) |
|||
String[] path() default {}; |
|||
|
|||
/** |
|||
* Alias for {@link RequestMapping#params}. |
|||
*/ |
|||
@AliasFor(annotation = RequestMapping.class) |
|||
String[] params() default {}; |
|||
|
|||
/** |
|||
* Alias for {@link RequestMapping#headers}. |
|||
*/ |
|||
@AliasFor(annotation = RequestMapping.class) |
|||
String[] headers() default {}; |
|||
|
|||
/** |
|||
* Alias for {@link RequestMapping#consumes}. |
|||
* |
|||
* @since 4.3.5 |
|||
*/ |
|||
@AliasFor(annotation = RequestMapping.class) |
|||
String[] consumes() default {}; |
|||
|
|||
/** |
|||
* Alias for {@link RequestMapping#produces}. |
|||
*/ |
|||
@AliasFor(annotation = RequestMapping.class) |
|||
String[] produces() default {}; |
|||
} |
@ -0,0 +1,38 @@ |
|||
package com.canvas.web.base; |
|||
|
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import org.apache.commons.lang3.builder.ToStringBuilder; |
|||
|
|||
import java.io.Serializable; |
|||
import java.lang.reflect.Field; |
|||
import java.sql.Timestamp; |
|||
|
|||
|
|||
@Getter |
|||
@Setter |
|||
public class BaseDTO implements Serializable { |
|||
|
|||
private String createBy; |
|||
|
|||
private String updatedBy; |
|||
|
|||
private Timestamp createTime; |
|||
|
|||
private Timestamp updateTime; |
|||
|
|||
@Override |
|||
public String toString() { |
|||
ToStringBuilder builder = new ToStringBuilder(this); |
|||
Field[] fields = this.getClass().getDeclaredFields(); |
|||
try { |
|||
for (Field f : fields) { |
|||
f.setAccessible(true); |
|||
builder.append(f.getName(), f.get(this)).append("\n"); |
|||
} |
|||
} catch (Exception e) { |
|||
builder.append("toString builder encounter an error"); |
|||
} |
|||
return builder.toString(); |
|||
} |
|||
} |
@ -0,0 +1,41 @@ |
|||
package com.canvas.web.config; |
|||
|
|||
|
|||
import lombok.Data; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.context.annotation.Configuration; |
|||
|
|||
@Configuration |
|||
@Data |
|||
public class CommonConfig { |
|||
|
|||
/** |
|||
* 图片域名 |
|||
*/ |
|||
public static String imageURL; |
|||
|
|||
/** |
|||
* 是否演示环境:true是,false否 |
|||
*/ |
|||
public static boolean appDebug; |
|||
|
|||
/** |
|||
* 图片域名赋值 |
|||
* |
|||
* @param url 域名地址 |
|||
*/ |
|||
@Value("${javaweb.image-url}") |
|||
public void setImageURL(String url) { |
|||
imageURL = url; |
|||
} |
|||
|
|||
/** |
|||
* 是否演示模式 |
|||
* |
|||
* @param debug |
|||
*/ |
|||
@Value("${javaweb.app-debug}") |
|||
public void setAppDebug(boolean debug) { |
|||
appDebug = debug; |
|||
} |
|||
} |
@ -0,0 +1,38 @@ |
|||
package com.canvas.web.config; |
|||
|
|||
|
|||
import lombok.Data; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.context.annotation.Configuration; |
|||
|
|||
@Configuration |
|||
@Data |
|||
public class UploadFileConfig { |
|||
/** |
|||
* 上传目录 |
|||
*/ |
|||
public static String uploadFolder; |
|||
/** |
|||
* 访问路径 |
|||
*/ |
|||
public static String staticAccessPath; |
|||
/** |
|||
* 上传服务器的映射文件夹 |
|||
*/ |
|||
public static String accessPath; |
|||
|
|||
@Value("${file.uploadFolder}") |
|||
public void setUploadFolder(String path) { |
|||
uploadFolder = path; |
|||
} |
|||
|
|||
@Value("${file.staticAccessPath}") |
|||
public void setStaticAccessPath(String path) { |
|||
staticAccessPath = path; |
|||
} |
|||
|
|||
@Value("${file.accessPath}") |
|||
public void setAccessPath(String path) { |
|||
accessPath = path; |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
package com.canvas.web.service; |
|||
|
|||
|
|||
import com.canvas.web.utils.JsonResult; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
|
|||
public interface IUplpadService { |
|||
|
|||
/** |
|||
* 上传图片 |
|||
* |
|||
* @param request 网络请求 |
|||
* @param name 目录名 |
|||
* @return |
|||
*/ |
|||
JsonResult uploadImage(HttpServletRequest request, String name); |
|||
|
|||
} |
@ -0,0 +1,30 @@ |
|||
package com.canvas.web.service.impl; |
|||
|
|||
import com.canvas.web.service.IUplpadService; |
|||
import com.canvas.web.utils.CommonUtils; |
|||
import com.canvas.web.utils.JsonResult; |
|||
import com.canvas.web.utils.UploadUtils; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
public class UploadServiceImpl implements IUplpadService { |
|||
|
|||
|
|||
/** |
|||
* 上传图片 |
|||
* |
|||
* @param request 网络请求 |
|||
* @param name 目录名 |
|||
* @return |
|||
*/ |
|||
@Override |
|||
public JsonResult uploadImage(HttpServletRequest request, String name) { |
|||
UploadUtils uploadUtils = new UploadUtils(); |
|||
Map<String, Object> result = uploadUtils.uploadFile(request, name); |
|||
List<String> imageList = (List<String>) result.get("image"); |
|||
String imageUrl = CommonUtils.getImageURL(imageList.get(0)); |
|||
return JsonResult.success("上传成功", imageUrl); |
|||
} |
|||
} |
@ -0,0 +1,39 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
/** |
|||
* 关于缓存的Key集合 |
|||
*/ |
|||
public interface CacheKey { |
|||
|
|||
/** |
|||
* 内置 用户、岗位、应用、菜单、角色 相关key |
|||
*/ |
|||
String USER_MODIFY_TIME_KEY = "user:modify:time:key:"; |
|||
String APP_MODIFY_TIME_KEY = "app:modify:time:key:"; |
|||
String JOB_MODIFY_TIME_KEY = "job:modify:time:key:"; |
|||
String MENU_MODIFY_TIME_KEY = "menu:modify:time:key:"; |
|||
String ROLE_MODIFY_TIME_KEY = "role:modify:time:key:"; |
|||
String DEPT_MODIFY_TIME_KEY = "dept:modify:time:key:"; |
|||
|
|||
/** |
|||
* 用户 |
|||
*/ |
|||
String USER_ID = "user::id:"; |
|||
String USER_NAME = "user::username:"; |
|||
/** |
|||
* 数据 |
|||
*/ |
|||
String DATE_USER = "data::user:"; |
|||
/** |
|||
* 菜单 |
|||
*/ |
|||
String MENU_USER = "menu::user:"; |
|||
/** |
|||
* 角色授权 |
|||
*/ |
|||
String ROLE_AUTH = "role::auth:"; |
|||
/** |
|||
* 角色信息 |
|||
*/ |
|||
String ROLE_ID = "role::id:"; |
|||
} |
@ -0,0 +1,13 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
public interface CallBack { |
|||
|
|||
//回调执行方法 |
|||
void executor(); |
|||
|
|||
|
|||
//本回调任务名称 |
|||
default String getCallBackName() { |
|||
return Thread.currentThread().getId() + ":" + this.getClass().getName(); |
|||
} |
|||
} |
@ -0,0 +1,294 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
import com.alibaba.fastjson.JSONObject; |
|||
import com.canvas.web.config.CommonConfig; |
|||
import org.springframework.util.StringUtils; |
|||
|
|||
import java.lang.reflect.Field; |
|||
import java.security.MessageDigest; |
|||
import java.text.NumberFormat; |
|||
import java.text.ParseException; |
|||
import java.text.SimpleDateFormat; |
|||
import java.util.Date; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.regex.Matcher; |
|||
import java.util.regex.Pattern; |
|||
|
|||
public class CommonUtils { |
|||
/** |
|||
* 分转元 |
|||
* |
|||
* @param amount |
|||
* @return |
|||
*/ |
|||
public static String fenToYuan(String amount) { |
|||
NumberFormat format = NumberFormat.getInstance(); |
|||
try { |
|||
Number number = format.parse(amount); |
|||
double temp = number.doubleValue() / 100.0; |
|||
format.setGroupingUsed(false); |
|||
// 设置返回的小数部分所允许的最大位数 |
|||
format.setMaximumFractionDigits(2); |
|||
amount = format.format(temp); |
|||
} catch (ParseException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
return amount; |
|||
} |
|||
|
|||
/** |
|||
* 元转分 |
|||
* |
|||
* @param amount |
|||
* @return |
|||
*/ |
|||
public static String yuanToFen(String amount) { |
|||
NumberFormat format = NumberFormat.getInstance(); |
|||
try { |
|||
Number number = format.parse(amount); |
|||
double temp = number.doubleValue() * 100.0; |
|||
format.setGroupingUsed(false); |
|||
// 设置返回数的小数部分所允许的最大位数 |
|||
format.setMaximumFractionDigits(0); |
|||
amount = format.format(temp); |
|||
} catch (ParseException e) { |
|||
e.printStackTrace(); |
|||
} |
|||
return amount; |
|||
} |
|||
|
|||
/** |
|||
* 获取当前时间时间戳 |
|||
* |
|||
* @return |
|||
*/ |
|||
public static Integer timeStamp() { |
|||
long currentTime = System.currentTimeMillis(); |
|||
String time = String.valueOf(currentTime / 1000); |
|||
return Integer.valueOf(time); |
|||
} |
|||
|
|||
/** |
|||
* 时间转为日期格式 |
|||
* |
|||
* @param time |
|||
* @param format |
|||
* @return |
|||
*/ |
|||
public static String formatTime(Integer time, String format) { |
|||
if (StringUtils.isEmpty(time)) { |
|||
return ""; |
|||
} |
|||
SimpleDateFormat dateFormat = new SimpleDateFormat(format); |
|||
long timeLong = time.longValue() * 1000; |
|||
Date date = new Date(timeLong); |
|||
return dateFormat.format(date); |
|||
} |
|||
|
|||
/** |
|||
* 获取到图片域名的地址 |
|||
* |
|||
* @param imageUrl |
|||
* @return |
|||
*/ |
|||
public static String getImageURL(String imageUrl) { |
|||
return CommonConfig.imageURL + imageUrl; |
|||
} |
|||
|
|||
/** |
|||
* 验证邮箱是否正确 |
|||
* |
|||
* @param email |
|||
* @return |
|||
*/ |
|||
public static boolean isEmail(String email) { |
|||
boolean flag = false; |
|||
try { |
|||
String check = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$"; |
|||
Pattern regex = Pattern.compile(check); |
|||
Matcher matcher = regex.matcher(email); |
|||
flag = matcher.matches(); |
|||
} catch (Exception e) { |
|||
flag = false; |
|||
} |
|||
return flag; |
|||
} |
|||
|
|||
/** |
|||
* 验证手机号是否正确 |
|||
* |
|||
* @param mobile |
|||
* @return |
|||
*/ |
|||
public static boolean isMobile(String mobile) { |
|||
boolean flag = false; |
|||
try { |
|||
Pattern p = Pattern.compile("^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$"); |
|||
Matcher m = p.matcher(mobile); |
|||
flag = m.matches(); |
|||
} catch (Exception e) { |
|||
flag = false; |
|||
} |
|||
return flag; |
|||
} |
|||
|
|||
/** |
|||
* 生成指定位数的随机字符串 |
|||
* |
|||
* @param isNum 是否是纯数字 |
|||
* @param length 长度 |
|||
* @return |
|||
*/ |
|||
public static String getRandomStr(boolean isNum, int length) { |
|||
String resultStr = ""; |
|||
String str = isNum ? "1234567890" : "1234567890abcdefghijkmnpqrstuvwxyz"; |
|||
int len = str.length(); |
|||
boolean isStop = true; |
|||
do { |
|||
resultStr = ""; |
|||
int count = 0; |
|||
for (int i = 0; i < length; i++) { |
|||
double dblR = Math.random() * len; |
|||
int intR = (int) Math.floor(dblR); |
|||
char c = str.charAt(intR); |
|||
if (('0' <= c) && (c <= '9')) { |
|||
count++; |
|||
} |
|||
resultStr += str.charAt(intR); |
|||
} |
|||
if (count >= 2) { |
|||
isStop = false; |
|||
} |
|||
} while (isStop); |
|||
return resultStr; |
|||
} |
|||
|
|||
/** |
|||
* 判断是否在数组中 |
|||
* |
|||
* @param s |
|||
* @param array |
|||
* @return |
|||
*/ |
|||
public static boolean inArray(final String s, final String[] array) { |
|||
for (String item : array) { |
|||
if (item != null && item.equals(s)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* 从html中提取纯文本 |
|||
* |
|||
* @param strHtml |
|||
* @return |
|||
*/ |
|||
public static String stripHtml(String strHtml) { |
|||
String content = strHtml.replaceAll("</?[^>]+>", ""); //剔出<html>的标签 |
|||
content = content.replaceAll("\\s*|\t|\r|\n", "");//去除字符串中的空格,回车,换行符,制表符 |
|||
return content; |
|||
} |
|||
|
|||
/** |
|||
* 去除字符串中的空格、回车、换行符、制表符等 |
|||
* |
|||
* @param str 原始字符串 |
|||
* @return |
|||
*/ |
|||
public static String replaceSpecialStr(String str) { |
|||
String repl = ""; |
|||
if (str != null) { |
|||
Pattern p = Pattern.compile("\\s*|\t|\r|\n"); |
|||
Matcher m = p.matcher(str); |
|||
repl = m.replaceAll(""); |
|||
} |
|||
return repl; |
|||
} |
|||
|
|||
/** |
|||
* 判断某个元素是否在数组中 |
|||
* |
|||
* @param key 元素 |
|||
* @param map 数组 |
|||
* @return |
|||
*/ |
|||
public static boolean inArray(String key, Map<String, String> map) { |
|||
boolean flag = false; |
|||
for (String k : map.keySet()) { |
|||
if (k.equals(key)) { |
|||
flag = true; |
|||
} |
|||
} |
|||
return flag; |
|||
} |
|||
|
|||
/** |
|||
* 对象转Map |
|||
* |
|||
* @param obj 对象 |
|||
* @return |
|||
* @throws IllegalAccessException |
|||
*/ |
|||
public static Map<String, Object> objectToMap(Object obj) throws IllegalAccessException { |
|||
Map<String, Object> map = new HashMap<>(); |
|||
Class<?> clazz = obj.getClass(); |
|||
for (Field field : clazz.getDeclaredFields()) { |
|||
field.setAccessible(true); |
|||
String fieldName = field.getName(); |
|||
Object value = field.get(obj); |
|||
map.put(fieldName, value); |
|||
} |
|||
return map; |
|||
} |
|||
|
|||
/** |
|||
* 判断是否是JSON格式 |
|||
* |
|||
* @param str JSON字符串 |
|||
* @return |
|||
*/ |
|||
private boolean isJson(String str) { |
|||
try { |
|||
JSONObject jsonStr = JSONObject.parseObject(str); |
|||
return true; |
|||
} catch (Exception e) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* MD5方法 |
|||
* |
|||
* @param source |
|||
* @return |
|||
*/ |
|||
public static String md5(byte[] source) { |
|||
try { |
|||
MessageDigest md = MessageDigest.getInstance("MD5"); |
|||
md.update(source); |
|||
StringBuffer buf = new StringBuffer(); |
|||
for (byte b : md.digest()) { |
|||
buf.append(String.format("%02x", b & 0xff)); |
|||
} |
|||
return buf.toString(); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 密码加密 |
|||
* |
|||
* @param password 密码 |
|||
* @return |
|||
*/ |
|||
public static String password(String password) { |
|||
String md51 = md5(password.getBytes()); |
|||
String pwd = md5((md51 + "IgtUdEQJyVevaCxQnY").getBytes()); |
|||
return pwd; |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
public class ElAdminConstant { |
|||
|
|||
/** |
|||
* 用于IP定位转换 |
|||
*/ |
|||
public static final String REGION = "内网IP|内网IP"; |
|||
/** |
|||
* win 系统 |
|||
*/ |
|||
public static final String WIN = "win"; |
|||
|
|||
/** |
|||
* mac 系统 |
|||
*/ |
|||
public static final String MAC = "mac"; |
|||
|
|||
/** |
|||
* 常用接口 |
|||
*/ |
|||
public static class Url { |
|||
// IP归属地查询 |
|||
public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true"; |
|||
} |
|||
} |
@ -0,0 +1,42 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
public enum ErrorCode { |
|||
|
|||
FAILED(1, "操作失败"), |
|||
TOKEN_MISSING(300, "token丢失"), |
|||
TOKEN_ERROR(301, "token认证失败"), |
|||
PARAM_MISSING(400, "参数丢失"), |
|||
PARAM_ERROR(401, "参数错误"), |
|||
SYSTEM_ERROR(500, "系统错误"), |
|||
UNKNOWN_ERROR(501, "未知错误"); |
|||
|
|||
public static final Integer MESSAGE_PARAM_MISSING = 400; |
|||
|
|||
/** |
|||
* 错误码 |
|||
*/ |
|||
private Integer code; |
|||
/** |
|||
* 错误描述 |
|||
*/ |
|||
private String msg; |
|||
|
|||
public Integer getCode() { |
|||
return this.code; |
|||
} |
|||
|
|||
public String getMsg() { |
|||
return this.msg; |
|||
} |
|||
|
|||
/** |
|||
* 构造函数 |
|||
* |
|||
* @param code |
|||
* @param msg |
|||
*/ |
|||
private ErrorCode(Integer code, String msg) { |
|||
this.code = code; |
|||
this.msg = msg; |
|||
} |
|||
} |
@ -0,0 +1,356 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
import cn.hutool.core.io.IoUtil; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import cn.hutool.poi.excel.BigExcelWriter; |
|||
import cn.hutool.poi.excel.ExcelUtil; |
|||
import com.canvas.web.exception.BaseException; |
|||
import org.apache.poi.ss.usermodel.CellType; |
|||
import org.apache.poi.util.IOUtils; |
|||
import org.apache.poi.xssf.streaming.SXSSFCell; |
|||
import org.apache.poi.xssf.streaming.SXSSFRow; |
|||
import org.apache.poi.xssf.streaming.SXSSFSheet; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import javax.servlet.ServletOutputStream; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.io.*; |
|||
import java.security.MessageDigest; |
|||
import java.text.DecimalFormat; |
|||
import java.text.SimpleDateFormat; |
|||
import java.util.Date; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
public class FileUtil extends cn.hutool.core.io.FileUtil{ |
|||
|
|||
private static final Logger log = LoggerFactory.getLogger(FileUtil.class); |
|||
|
|||
/** |
|||
* 系统临时目录 |
|||
* <br> |
|||
* windows 包含路径分割符,但Linux 不包含, |
|||
* 在windows \\==\ 前提下, |
|||
* 为安全起见 同意拼装 路径分割符, |
|||
* <pre> |
|||
* java.io.tmpdir |
|||
* windows : C:\Users/xxx\AppData\Local\Temp\ |
|||
* linux: /temp |
|||
* </pre> |
|||
*/ |
|||
public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator; |
|||
/** |
|||
* 定义GB的计算常量 |
|||
*/ |
|||
private static final int GB = 1024 * 1024 * 1024; |
|||
/** |
|||
* 定义MB的计算常量 |
|||
*/ |
|||
private static final int MB = 1024 * 1024; |
|||
/** |
|||
* 定义KB的计算常量 |
|||
*/ |
|||
private static final int KB = 1024; |
|||
|
|||
/** |
|||
* 格式化小数 |
|||
*/ |
|||
private static final DecimalFormat DF = new DecimalFormat("0.00"); |
|||
|
|||
public static final String IMAGE = "图片"; |
|||
public static final String TXT = "文档"; |
|||
public static final String MUSIC = "音乐"; |
|||
public static final String VIDEO = "视频"; |
|||
public static final String OTHER = "其他"; |
|||
|
|||
|
|||
/** |
|||
* MultipartFile转File |
|||
*/ |
|||
public static File toFile(MultipartFile multipartFile) { |
|||
// 获取文件名 |
|||
String fileName = multipartFile.getOriginalFilename(); |
|||
// 获取文件后缀 |
|||
String prefix = "." + getExtensionName(fileName); |
|||
File file = null; |
|||
try { |
|||
// 用uuid作为文件名,防止生成的临时文件重复 |
|||
file = File.createTempFile(IdUtil.simpleUUID(), prefix); |
|||
// MultipartFile to File |
|||
multipartFile.transferTo(file); |
|||
} catch (IOException e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
return file; |
|||
} |
|||
|
|||
/** |
|||
* 获取文件扩展名,不带 . |
|||
*/ |
|||
public static String getExtensionName(String filename) { |
|||
if ((filename != null) && (filename.length() > 0)) { |
|||
int dot = filename.lastIndexOf('.'); |
|||
if ((dot > -1) && (dot < (filename.length() - 1))) { |
|||
return filename.substring(dot + 1); |
|||
} |
|||
} |
|||
return filename; |
|||
} |
|||
|
|||
/** |
|||
* Java文件操作 获取不带扩展名的文件名 |
|||
*/ |
|||
public static String getFileNameNoEx(String filename) { |
|||
if ((filename != null) && (filename.length() > 0)) { |
|||
int dot = filename.lastIndexOf('.'); |
|||
if ((dot > -1) && (dot < (filename.length()))) { |
|||
return filename.substring(0, dot); |
|||
} |
|||
} |
|||
return filename; |
|||
} |
|||
|
|||
/** |
|||
* 文件大小转换 |
|||
*/ |
|||
public static String getSize(long size) { |
|||
String resultSize; |
|||
if (size / GB >= 1) { |
|||
//如果当前Byte的值大于等于1GB |
|||
resultSize = DF.format(size / (float) GB) + "GB "; |
|||
} else if (size / MB >= 1) { |
|||
//如果当前Byte的值大于等于1MB |
|||
resultSize = DF.format(size / (float) MB) + "MB "; |
|||
} else if (size / KB >= 1) { |
|||
//如果当前Byte的值大于等于1KB |
|||
resultSize = DF.format(size / (float) KB) + "KB "; |
|||
} else { |
|||
resultSize = size + "B "; |
|||
} |
|||
return resultSize; |
|||
} |
|||
|
|||
/** |
|||
* inputStream 转 File |
|||
*/ |
|||
static File inputStreamToFile(InputStream ins, String name) throws Exception { |
|||
File file = new File(SYS_TEM_DIR + name); |
|||
if (file.exists()) { |
|||
return file; |
|||
} |
|||
OutputStream os = new FileOutputStream(file); |
|||
int bytesRead; |
|||
int len = 8192; |
|||
byte[] buffer = new byte[len]; |
|||
while ((bytesRead = ins.read(buffer, 0, len)) != -1) { |
|||
os.write(buffer, 0, bytesRead); |
|||
} |
|||
os.close(); |
|||
ins.close(); |
|||
return file; |
|||
} |
|||
|
|||
/** |
|||
* 将文件名解析成文件的上传路径 |
|||
*/ |
|||
public static File upload(MultipartFile file, String filePath) { |
|||
Date date = new Date(); |
|||
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS"); |
|||
String name = getFileNameNoEx(file.getOriginalFilename()); |
|||
String suffix = getExtensionName(file.getOriginalFilename()); |
|||
String nowStr = "-" + format.format(date); |
|||
try { |
|||
String fileName = name + nowStr + "." + suffix; |
|||
String path = filePath + fileName; |
|||
// getCanonicalFile 可解析正确各种路径 |
|||
File dest = new File(path).getCanonicalFile(); |
|||
// 检测是否存在目录 |
|||
if (!dest.getParentFile().exists()) { |
|||
if (!dest.getParentFile().mkdirs()) { |
|||
System.out.println("was not successful."); |
|||
} |
|||
} |
|||
// 文件写入 |
|||
file.transferTo(dest); |
|||
return dest; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 导出excel |
|||
*/ |
|||
public static void downloadExcel(List<Map<String, Object>> list, HttpServletResponse response) throws IOException { |
|||
String tempPath = SYS_TEM_DIR + IdUtil.fastSimpleUUID() + ".xlsx"; |
|||
File file = new File(tempPath); |
|||
BigExcelWriter writer = ExcelUtil.getBigWriter(file); |
|||
// 一次性写出内容,使用默认样式,强制输出标题 |
|||
writer.write(list, true); |
|||
SXSSFSheet sheet = (SXSSFSheet)writer.getSheet(); |
|||
//上面需要强转SXSSFSheet 不然没有trackAllColumnsForAutoSizing方法 |
|||
sheet.trackAllColumnsForAutoSizing(); |
|||
//列宽自适应 |
|||
writer.autoSizeColumnAll(); |
|||
//列宽自适应支持中文单元格 |
|||
sizeChineseColumn(sheet, writer); |
|||
//response为HttpServletResponse对象 |
|||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); |
|||
//test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 |
|||
response.setHeader("Content-Disposition", "attachment;filename=file.xlsx"); |
|||
ServletOutputStream out = response.getOutputStream(); |
|||
// 终止后删除临时文件 |
|||
file.deleteOnExit(); |
|||
writer.flush(out, true); |
|||
//此处记得关闭输出Servlet流 |
|||
IoUtil.close(out); |
|||
} |
|||
|
|||
/** |
|||
* 自适应宽度(中文支持) |
|||
*/ |
|||
private static void sizeChineseColumn(SXSSFSheet sheet, BigExcelWriter writer) { |
|||
for (int columnNum = 0; columnNum < writer.getColumnCount(); columnNum++) { |
|||
int columnWidth = sheet.getColumnWidth(columnNum) / 256; |
|||
for (int rowNum = 0; rowNum < sheet.getLastRowNum(); rowNum++) { |
|||
SXSSFRow currentRow; |
|||
if (sheet.getRow(rowNum) == null) { |
|||
currentRow = sheet.createRow(rowNum); |
|||
} else { |
|||
currentRow = sheet.getRow(rowNum); |
|||
} |
|||
if (currentRow.getCell(columnNum) != null) { |
|||
SXSSFCell currentCell = currentRow.getCell(columnNum); |
|||
if (currentCell.getCellTypeEnum() == CellType.STRING) { |
|||
int length = currentCell.getStringCellValue().getBytes().length; |
|||
if (columnWidth < length) { |
|||
columnWidth = length; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
sheet.setColumnWidth(columnNum, columnWidth * 256); |
|||
} |
|||
} |
|||
|
|||
public static String getFileType(String type) { |
|||
String documents = "txt doc pdf ppt pps xlsx xls docx"; |
|||
String music = "mp3 wav wma mpa ram ra aac aif m4a"; |
|||
String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg"; |
|||
String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg"; |
|||
if (image.contains(type)) { |
|||
return IMAGE; |
|||
} else if (documents.contains(type)) { |
|||
return TXT; |
|||
} else if (music.contains(type)) { |
|||
return MUSIC; |
|||
} else if (video.contains(type)) { |
|||
return VIDEO; |
|||
} else { |
|||
return OTHER; |
|||
} |
|||
} |
|||
|
|||
public static void checkSize(long maxSize, long size) { |
|||
// 1M |
|||
int len = 1024 * 1024; |
|||
if (size > (maxSize * len)) { |
|||
throw new BaseException("文件超出规定大小"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 判断两个文件是否相同 |
|||
*/ |
|||
public static boolean check(File file1, File file2) { |
|||
String img1Md5 = getMd5(file1); |
|||
String img2Md5 = getMd5(file2); |
|||
return img1Md5.equals(img2Md5); |
|||
} |
|||
|
|||
/** |
|||
* 判断两个文件是否相同 |
|||
*/ |
|||
public static boolean check(String file1Md5, String file2Md5) { |
|||
return file1Md5.equals(file2Md5); |
|||
} |
|||
|
|||
private static byte[] getByte(File file) { |
|||
// 得到文件长度 |
|||
byte[] b = new byte[(int) file.length()]; |
|||
try { |
|||
InputStream in = new FileInputStream(file); |
|||
try { |
|||
System.out.println(in.read(b)); |
|||
} catch (IOException e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
} catch (FileNotFoundException e) { |
|||
log.error(e.getMessage(), e); |
|||
return null; |
|||
} |
|||
return b; |
|||
} |
|||
|
|||
private static String getMd5(byte[] bytes) { |
|||
// 16进制字符 |
|||
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; |
|||
try { |
|||
MessageDigest mdTemp = MessageDigest.getInstance("MD5"); |
|||
mdTemp.update(bytes); |
|||
byte[] md = mdTemp.digest(); |
|||
int j = md.length; |
|||
char[] str = new char[j * 2]; |
|||
int k = 0; |
|||
// 移位 输出字符串 |
|||
for (byte byte0 : md) { |
|||
str[k++] = hexDigits[byte0 >>> 4 & 0xf]; |
|||
str[k++] = hexDigits[byte0 & 0xf]; |
|||
} |
|||
return new String(str); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 下载文件 |
|||
* |
|||
* @param request / |
|||
* @param response / |
|||
* @param file / |
|||
*/ |
|||
public static void downloadFile(HttpServletRequest request, HttpServletResponse response, File file, boolean deleteOnExit) { |
|||
response.setCharacterEncoding(request.getCharacterEncoding()); |
|||
response.setContentType("application/octet-stream"); |
|||
FileInputStream fis = null; |
|||
try { |
|||
fis = new FileInputStream(file); |
|||
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName()); |
|||
IOUtils.copy(fis, response.getOutputStream()); |
|||
response.flushBuffer(); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
} finally { |
|||
if (fis != null) { |
|||
try { |
|||
fis.close(); |
|||
if (deleteOnExit) { |
|||
file.deleteOnExit(); |
|||
} |
|||
} catch (IOException e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static String getMd5(File file) { |
|||
return getMd5(getByte(file)); |
|||
} |
|||
} |
@ -0,0 +1,112 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
import org.springframework.http.HttpStatus; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
public class JsonResult implements Serializable { |
|||
|
|||
// 错误码 |
|||
private Integer code= HttpStatus.OK.value(); |
|||
|
|||
//提示语 |
|||
private String msg= "操作成功"; |
|||
|
|||
//返回对象 |
|||
private Object data; |
|||
|
|||
public Integer getCode() { |
|||
return this.code; |
|||
} |
|||
|
|||
public void setCode(final Integer code) { |
|||
this.code = code; |
|||
} |
|||
|
|||
public String getMsg() { |
|||
return this.msg; |
|||
} |
|||
|
|||
public void setMsg(final String msg) { |
|||
this.msg = msg; |
|||
} |
|||
|
|||
public Object getData() { |
|||
return this.data; |
|||
} |
|||
|
|||
public void setData(final Object data) { |
|||
this.data = data; |
|||
} |
|||
|
|||
/** |
|||
* 构造函数 |
|||
*/ |
|||
public JsonResult() { |
|||
} |
|||
|
|||
public JsonResult(String msg) { |
|||
this.msg = msg; |
|||
} |
|||
|
|||
public JsonResult(Object data) { |
|||
this.data = data; |
|||
} |
|||
|
|||
public JsonResult(Integer code, String msg) { |
|||
this.code = code; |
|||
this.msg = msg; |
|||
} |
|||
|
|||
public JsonResult(Integer code, String msg, Object data) { |
|||
this.code = code; |
|||
this.msg = msg; |
|||
this.data = data; |
|||
} |
|||
|
|||
public static JsonResult success() { |
|||
return new JsonResult(); |
|||
} |
|||
|
|||
public static JsonResult success(String msg) { |
|||
return new JsonResult(msg); |
|||
} |
|||
|
|||
public JsonResult success(Object data) { |
|||
return new JsonResult(HttpStatus.OK.value(), msg, data); |
|||
} |
|||
|
|||
public static JsonResult success(String msg, Object data) { |
|||
return new JsonResult(HttpStatus.OK.value(), msg, data); |
|||
} |
|||
|
|||
public static JsonResult error() { |
|||
return new JsonResult(-1, "操作失败"); |
|||
} |
|||
|
|||
public static JsonResult error(String msg) { |
|||
return new JsonResult(-1, msg); |
|||
} |
|||
|
|||
public static JsonResult error(Integer code, String msg) { |
|||
return new JsonResult(code, msg); |
|||
} |
|||
|
|||
public static JsonResult error(Integer code, String msg, Object data) { |
|||
return new JsonResult(code, msg, data); |
|||
} |
|||
|
|||
public static JsonResult error(ErrorCode errorCode) { |
|||
return new JsonResult(errorCode.getCode(), errorCode.getMsg()); |
|||
} |
|||
|
|||
public static JsonResult error(HttpStatus httpStatus, String msg, Object data) { |
|||
return new JsonResult(httpStatus.value(), msg, data); |
|||
} |
|||
|
|||
public Object error(HttpStatus httpStatus, String msg) { |
|||
this.code = httpStatus.value(); |
|||
this.msg = msg; |
|||
return this; |
|||
} |
|||
} |
@ -0,0 +1,696 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
import com.google.common.collect.Lists; |
|||
import com.google.common.collect.Sets; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.data.redis.connection.RedisConnection; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.data.redis.core.Cursor; |
|||
import org.springframework.data.redis.core.RedisConnectionUtils; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
import org.springframework.data.redis.core.ScanOptions; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.transaction.annotation.Propagation; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
import org.springframework.util.CollectionUtils; |
|||
|
|||
import java.util.*; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Component |
|||
public class RedisUtils { |
|||
|
|||
private static final Logger log = LoggerFactory.getLogger(RedisUtils.class); |
|||
private RedisTemplate<Object, Object> redisTemplate; |
|||
@Value("${jwt.online-key}") |
|||
private String onlineKey; |
|||
|
|||
public RedisUtils(RedisTemplate<Object, Object> redisTemplate) { |
|||
this.redisTemplate = redisTemplate; |
|||
} |
|||
|
|||
/** |
|||
* 指定缓存失效时间 |
|||
* |
|||
* @param key 键 |
|||
* @param time 时间(秒) |
|||
*/ |
|||
public boolean expire(String key, long time) { |
|||
try { |
|||
if (time > 0) { |
|||
redisTemplate.expire(key, time, TimeUnit.SECONDS); |
|||
} |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* 指定缓存失效时间 |
|||
* |
|||
* @param key 键 |
|||
* @param time 时间(秒) |
|||
* @param timeUnit 单位 |
|||
*/ |
|||
public boolean expire(String key, long time, TimeUnit timeUnit) { |
|||
try { |
|||
if (time > 0) { |
|||
redisTemplate.expire(key, time, timeUnit); |
|||
} |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* 根据 key 获取过期时间 |
|||
* |
|||
* @param key 键 不能为null |
|||
* @return 时间(秒) 返回0代表为永久有效 |
|||
*/ |
|||
public long getExpire(Object key) { |
|||
return redisTemplate.getExpire(key, TimeUnit.SECONDS); |
|||
} |
|||
|
|||
/** |
|||
* 查找匹配key |
|||
* |
|||
* @param pattern key |
|||
* @return / |
|||
*/ |
|||
public List<String> scan(String pattern) { |
|||
ScanOptions options = ScanOptions.scanOptions().match(pattern).build(); |
|||
RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); |
|||
RedisConnection rc = Objects.requireNonNull(factory).getConnection(); |
|||
Cursor<byte[]> cursor = rc.scan(options); |
|||
List<String> result = new ArrayList<>(); |
|||
while (cursor.hasNext()) { |
|||
result.add(new String(cursor.next())); |
|||
} |
|||
try { |
|||
RedisConnectionUtils.releaseConnection(rc, factory); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 分页查询 key |
|||
* |
|||
* @param patternKey key |
|||
* @param page 页码 |
|||
* @param size 每页数目 |
|||
* @return / |
|||
*/ |
|||
public List<String> findKeysForPage(String patternKey, int page, int size) { |
|||
ScanOptions options = ScanOptions.scanOptions().match(patternKey).build(); |
|||
RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); |
|||
RedisConnection rc = Objects.requireNonNull(factory).getConnection(); |
|||
Cursor<byte[]> cursor = rc.scan(options); |
|||
List<String> result = new ArrayList<>(size); |
|||
int tmpIndex = 0; |
|||
int fromIndex = page * size; |
|||
int toIndex = page * size + size; |
|||
while (cursor.hasNext()) { |
|||
if (tmpIndex >= fromIndex && tmpIndex < toIndex) { |
|||
result.add(new String(cursor.next())); |
|||
tmpIndex++; |
|||
continue; |
|||
} |
|||
// 获取到满足条件的数据后,就可以退出了 |
|||
if (tmpIndex >= toIndex) { |
|||
break; |
|||
} |
|||
tmpIndex++; |
|||
cursor.next(); |
|||
} |
|||
try { |
|||
RedisConnectionUtils.releaseConnection(rc, factory); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 判断key是否存在 |
|||
* |
|||
* @param key 键 |
|||
* @return true 存在 false不存在 |
|||
*/ |
|||
public boolean hasKey(String key) { |
|||
try { |
|||
return redisTemplate.hasKey(key); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 删除缓存 |
|||
* |
|||
* @param keys 可以传一个值 或多个 |
|||
*/ |
|||
public void del(String... keys) { |
|||
if (keys != null && keys.length > 0) { |
|||
if (keys.length == 1) { |
|||
boolean result = redisTemplate.delete(keys[0]); |
|||
log.debug("--------------------------------------------"); |
|||
log.debug(new StringBuilder("删除缓存:").append(keys[0]).append(",结果:").append(result).toString()); |
|||
log.debug("--------------------------------------------"); |
|||
} else { |
|||
Set<Object> keySet = new HashSet<>(); |
|||
for (String key : keys) { |
|||
keySet.addAll(redisTemplate.keys(key)); |
|||
} |
|||
long count = redisTemplate.delete(keySet); |
|||
log.debug("--------------------------------------------"); |
|||
log.debug("成功删除缓存:" + keySet.toString()); |
|||
log.debug("缓存删除数量:" + count + "个"); |
|||
log.debug("--------------------------------------------"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ============================String============================= |
|||
|
|||
/** |
|||
* 普通缓存获取 |
|||
* |
|||
* @param key 键 |
|||
* @return 值 |
|||
*/ |
|||
public Object get(String key) { |
|||
return key == null ? null : redisTemplate.opsForValue().get(key); |
|||
} |
|||
|
|||
/** |
|||
* 批量获取 |
|||
* |
|||
* @param keys |
|||
* @return |
|||
*/ |
|||
public List<Object> multiGet(List<String> keys) { |
|||
List list = redisTemplate.opsForValue().multiGet(Sets.newHashSet(keys)); |
|||
List resultList = Lists.newArrayList(); |
|||
Optional.ofNullable(list).ifPresent(e-> list.forEach(ele-> Optional.ofNullable(ele).ifPresent(resultList::add))); |
|||
return resultList; |
|||
} |
|||
|
|||
/** |
|||
* 普通缓存放入 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @return true成功 false失败 |
|||
*/ |
|||
public boolean set(String key, Object value) { |
|||
try { |
|||
redisTemplate.opsForValue().set(key, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 普通缓存放入并设置时间 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 |
|||
* @return true成功 false 失败 |
|||
*/ |
|||
public boolean set(String key, Object value, long time) { |
|||
try { |
|||
if (time > 0) { |
|||
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); |
|||
} else { |
|||
set(key, value); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 普通缓存放入并设置时间 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @param time 时间 |
|||
* @param timeUnit 类型 |
|||
* @return true成功 false 失败 |
|||
*/ |
|||
public boolean set(String key, Object value, long time, TimeUnit timeUnit) { |
|||
try { |
|||
if (time > 0) { |
|||
redisTemplate.opsForValue().set(key, value, time, timeUnit); |
|||
} else { |
|||
set(key, value); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// ================================Map================================= |
|||
|
|||
/** |
|||
* HashGet |
|||
* |
|||
* @param key 键 不能为null |
|||
* @param item 项 不能为null |
|||
* @return 值 |
|||
*/ |
|||
public Object hget(String key, String item) { |
|||
return redisTemplate.opsForHash().get(key, item); |
|||
} |
|||
|
|||
/** |
|||
* 获取hashKey对应的所有键值 |
|||
* |
|||
* @param key 键 |
|||
* @return 对应的多个键值 |
|||
*/ |
|||
public Map<Object, Object> hmget(String key) { |
|||
return redisTemplate.opsForHash().entries(key); |
|||
|
|||
} |
|||
|
|||
/** |
|||
* HashSet |
|||
* |
|||
* @param key 键 |
|||
* @param map 对应多个键值 |
|||
* @return true 成功 false 失败 |
|||
*/ |
|||
public boolean hmset(String key, Map<String, Object> map) { |
|||
try { |
|||
redisTemplate.opsForHash().putAll(key, map); |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* HashSet 并设置时间 |
|||
* |
|||
* @param key 键 |
|||
* @param map 对应多个键值 |
|||
* @param time 时间(秒) |
|||
* @return true成功 false失败 |
|||
*/ |
|||
public boolean hmset(String key, Map<String, Object> map, long time) { |
|||
try { |
|||
redisTemplate.opsForHash().putAll(key, map); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 向一张hash表中放入数据,如果不存在将创建 |
|||
* |
|||
* @param key 键 |
|||
* @param item 项 |
|||
* @param value 值 |
|||
* @return true 成功 false失败 |
|||
*/ |
|||
public boolean hset(String key, String item, Object value) { |
|||
try { |
|||
redisTemplate.opsForHash().put(key, item, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 向一张hash表中放入数据,如果不存在将创建 |
|||
* |
|||
* @param key 键 |
|||
* @param item 项 |
|||
* @param value 值 |
|||
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 |
|||
* @return true 成功 false失败 |
|||
*/ |
|||
public boolean hset(String key, String item, Object value, long time) { |
|||
try { |
|||
redisTemplate.opsForHash().put(key, item, value); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 删除hash表中的值 |
|||
* |
|||
* @param key 键 不能为null |
|||
* @param item 项 可以使多个 不能为null |
|||
*/ |
|||
public void hdel(String key, Object... item) { |
|||
redisTemplate.opsForHash().delete(key, item); |
|||
} |
|||
|
|||
/** |
|||
* 判断hash表中是否有该项的值 |
|||
* |
|||
* @param key 键 不能为null |
|||
* @param item 项 不能为null |
|||
* @return true 存在 false不存在 |
|||
*/ |
|||
public boolean hHasKey(String key, String item) { |
|||
return redisTemplate.opsForHash().hasKey(key, item); |
|||
} |
|||
|
|||
/** |
|||
* hash递增 如果不存在,就会创建一个 并把新增后的值返回 |
|||
* |
|||
* @param key 键 |
|||
* @param item 项 |
|||
* @param by 要增加几(大于0) |
|||
* @return |
|||
*/ |
|||
public double hincr(String key, String item, double by) { |
|||
return redisTemplate.opsForHash().increment(key, item, by); |
|||
} |
|||
|
|||
/** |
|||
* hash递减 |
|||
* |
|||
* @param key 键 |
|||
* @param item 项 |
|||
* @param by 要减少记(小于0) |
|||
* @return |
|||
*/ |
|||
public double hdecr(String key, String item, double by) { |
|||
return redisTemplate.opsForHash().increment(key, item, -by); |
|||
} |
|||
|
|||
// ============================set============================= |
|||
|
|||
/** |
|||
* 根据key获取Set中的所有值 |
|||
* |
|||
* @param key 键 |
|||
* @return |
|||
*/ |
|||
public Set<Object> sGet(String key) { |
|||
try { |
|||
return redisTemplate.opsForSet().members(key); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据value从一个set中查询,是否存在 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @return true 存在 false不存在 |
|||
*/ |
|||
public boolean sHasKey(String key, Object value) { |
|||
try { |
|||
return redisTemplate.opsForSet().isMember(key, value); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将数据放入set缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param values 值 可以是多个 |
|||
* @return 成功个数 |
|||
*/ |
|||
public long sSet(String key, Object... values) { |
|||
try { |
|||
return redisTemplate.opsForSet().add(key, values); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将set数据放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param time 时间(秒) |
|||
* @param values 值 可以是多个 |
|||
* @return 成功个数 |
|||
*/ |
|||
public long sSetAndTime(String key, long time, Object... values) { |
|||
try { |
|||
Long count = redisTemplate.opsForSet().add(key, values); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return count; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取set缓存的长度 |
|||
* |
|||
* @param key 键 |
|||
* @return |
|||
*/ |
|||
public long sGetSetSize(String key) { |
|||
try { |
|||
return redisTemplate.opsForSet().size(key); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 移除值为value的 |
|||
* |
|||
* @param key 键 |
|||
* @param values 值 可以是多个 |
|||
* @return 移除的个数 |
|||
*/ |
|||
public long setRemove(String key, Object... values) { |
|||
try { |
|||
Long count = redisTemplate.opsForSet().remove(key, values); |
|||
return count; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
// ===============================list================================= |
|||
|
|||
/** |
|||
* 获取list缓存的内容 |
|||
* |
|||
* @param key 键 |
|||
* @param start 开始 |
|||
* @param end 结束 0 到 -1代表所有值 |
|||
* @return |
|||
*/ |
|||
public List<Object> lGet(String key, long start, long end) { |
|||
try { |
|||
return redisTemplate.opsForList().range(key, start, end); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取list缓存的长度 |
|||
* |
|||
* @param key 键 |
|||
* @return |
|||
*/ |
|||
public long lGetListSize(String key) { |
|||
try { |
|||
return redisTemplate.opsForList().size(key); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 通过索引 获取list中的值 |
|||
* |
|||
* @param key 键 |
|||
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 |
|||
* @return |
|||
*/ |
|||
public Object lGetIndex(String key, long index) { |
|||
try { |
|||
return redisTemplate.opsForList().index(key, index); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将list放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @return |
|||
*/ |
|||
public boolean lSet(String key, Object value) { |
|||
try { |
|||
redisTemplate.opsForList().rightPush(key, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将list放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @param time 时间(秒) |
|||
* @return |
|||
*/ |
|||
public boolean lSet(String key, Object value, long time) { |
|||
try { |
|||
redisTemplate.opsForList().rightPush(key, value); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将list放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @return |
|||
*/ |
|||
public boolean lSet(String key, List<Object> value) { |
|||
try { |
|||
redisTemplate.opsForList().rightPushAll(key, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将list放入缓存 |
|||
* |
|||
* @param key 键 |
|||
* @param value 值 |
|||
* @param time 时间(秒) |
|||
* @return |
|||
*/ |
|||
public boolean lSet(String key, List<Object> value, long time) { |
|||
try { |
|||
redisTemplate.opsForList().rightPushAll(key, value); |
|||
if (time > 0) { |
|||
expire(key, time); |
|||
} |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据索引修改list中的某条数据 |
|||
* |
|||
* @param key 键 |
|||
* @param index 索引 |
|||
* @param value 值 |
|||
* @return / |
|||
*/ |
|||
public boolean lUpdateIndex(String key, long index, Object value) { |
|||
try { |
|||
redisTemplate.opsForList().set(key, index, value); |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 移除N个值为value |
|||
* |
|||
* @param key 键 |
|||
* @param count 移除多少个 |
|||
* @param value 值 |
|||
* @return 移除的个数 |
|||
*/ |
|||
public long lRemove(String key, long count, Object value) { |
|||
try { |
|||
return redisTemplate.opsForList().remove(key, count, value); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @param prefix 前缀 |
|||
* @param ids id |
|||
*/ |
|||
public void delByKeys(String prefix, Set<Long> ids) { |
|||
Set<Object> keys = new HashSet<>(); |
|||
for (Long id : ids) { |
|||
keys.addAll(redisTemplate.keys(new StringBuffer(prefix).append(id).toString())); |
|||
} |
|||
long count = redisTemplate.delete(keys); |
|||
// 此处提示可自行删除 |
|||
log.debug("--------------------------------------------"); |
|||
log.debug("成功删除缓存:" + keys.toString()); |
|||
log.debug("缓存删除数量:" + count + "个"); |
|||
log.debug("--------------------------------------------"); |
|||
} |
|||
} |
@ -0,0 +1,121 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.BeansException; |
|||
import org.springframework.beans.factory.DisposableBean; |
|||
import org.springframework.context.ApplicationContext; |
|||
import org.springframework.context.ApplicationContextAware; |
|||
import org.springframework.core.env.Environment; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
public class SpringContextHolder implements ApplicationContextAware, DisposableBean { |
|||
|
|||
|
|||
private static ApplicationContext applicationContext = null; |
|||
private static final List<CallBack> CALL_BACKS = new ArrayList<>(); |
|||
private static boolean addCallback = true; |
|||
|
|||
public synchronized static void addCallBacks(CallBack callBack){ |
|||
if (addCallback){ |
|||
SpringContextHolder.CALL_BACKS.add(callBack); |
|||
}else { |
|||
log.warn("CallBack:{} 已无法添加!立即执行",callBack.getCallBackName()); |
|||
callBack.executor(); |
|||
} |
|||
} |
|||
|
|||
public static <T> T getBean(String name) { |
|||
assertContextInjected(); |
|||
return (T) applicationContext.getBean(name); |
|||
} |
|||
|
|||
/** |
|||
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. |
|||
*/ |
|||
public static <T> T getBean(Class<T> requiredType) { |
|||
assertContextInjected(); |
|||
return applicationContext.getBean(requiredType); |
|||
} |
|||
|
|||
/** |
|||
* 获取SpringBoot 配置信息 |
|||
* |
|||
* @param property 属性key |
|||
* @param defaultValue 默认值 |
|||
* @param requiredType 返回类型 |
|||
* @return / |
|||
*/ |
|||
public static <T> T getProperties(String property, T defaultValue, Class<T> requiredType) { |
|||
T result = defaultValue; |
|||
try { |
|||
result = getBean(Environment.class).getProperty(property, requiredType); |
|||
} catch (Exception ignored) {} |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 获取SpringBoot 配置信息 |
|||
* |
|||
* @param property 属性key |
|||
* @return / |
|||
*/ |
|||
public static String getProperties(String property) { |
|||
return getProperties(property, null, String.class); |
|||
} |
|||
|
|||
/** |
|||
* 获取SpringBoot 配置信息 |
|||
* |
|||
* @param property 属性key |
|||
* @param requiredType 返回类型 |
|||
* @return / |
|||
*/ |
|||
public static <T> T getProperties(String property, Class<T> requiredType) { |
|||
return getProperties(property, null, requiredType); |
|||
} |
|||
|
|||
/** |
|||
* 检查ApplicationContext不为空. |
|||
*/ |
|||
private static void assertContextInjected() { |
|||
if (applicationContext == null) { |
|||
throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" + |
|||
".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder."); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清除SpringContextHolder中的ApplicationContext为Null. |
|||
*/ |
|||
private static void clearHolder() { |
|||
log.debug("清除SpringContextHolder中的ApplicationContext:" |
|||
+ applicationContext); |
|||
applicationContext = null; |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
SpringContextHolder.clearHolder(); |
|||
} |
|||
|
|||
@Override |
|||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { |
|||
if (SpringContextHolder.applicationContext != null) { |
|||
log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringContextHolder.applicationContext); |
|||
} |
|||
SpringContextHolder.applicationContext = applicationContext; |
|||
if (addCallback) { |
|||
for (CallBack callBack : SpringContextHolder.CALL_BACKS) { |
|||
callBack.executor(); |
|||
} |
|||
CALL_BACKS.clear(); |
|||
} |
|||
SpringContextHolder.addCallback = false; |
|||
} |
|||
} |
@ -0,0 +1,268 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
|
|||
import cn.hutool.http.HttpUtil; |
|||
import cn.hutool.json.JSONObject; |
|||
import cn.hutool.json.JSONUtil; |
|||
import eu.bitwalker.useragentutils.Browser; |
|||
import eu.bitwalker.useragentutils.UserAgent; |
|||
import org.lionsoul.ip2region.DataBlock; |
|||
import org.lionsoul.ip2region.DbConfig; |
|||
import org.lionsoul.ip2region.DbSearcher; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.core.io.ClassPathResource; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.io.File; |
|||
import java.net.InetAddress; |
|||
import java.net.NetworkInterface; |
|||
import java.net.UnknownHostException; |
|||
import java.util.Calendar; |
|||
import java.util.Date; |
|||
import java.util.Enumeration; |
|||
|
|||
public class StringUtils extends org.apache.commons.lang3.StringUtils{ |
|||
|
|||
private static final Logger log = LoggerFactory.getLogger(StringUtils.class); |
|||
private static boolean ipLocal = false; |
|||
private static File file = null; |
|||
private static DbConfig config; |
|||
private static final char SEPARATOR = '_'; |
|||
private static final String UNKNOWN = "unknown"; |
|||
|
|||
static { |
|||
SpringContextHolder.addCallBacks(() -> { |
|||
StringUtils.ipLocal = SpringContextHolder.getProperties("ip.local-parsing", false, Boolean.class); |
|||
if (ipLocal) { |
|||
/* |
|||
* 此文件为独享 ,不必关闭 |
|||
*/ |
|||
String path = "ip2region/ip2region.db"; |
|||
String name = "ip2region.db"; |
|||
try { |
|||
config = new DbConfig(); |
|||
file = FileUtil.inputStreamToFile(new ClassPathResource(path).getInputStream(), name); |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 驼峰命名法工具 |
|||
* |
|||
* @return toCamelCase(" hello_world ") == "helloWorld" |
|||
* toCapitalizeCamelCase("hello_world") == "HelloWorld" |
|||
* toUnderScoreCase("helloWorld") = "hello_world" |
|||
*/ |
|||
public static String toCamelCase(String s) { |
|||
if (s == null) { |
|||
return null; |
|||
} |
|||
|
|||
s = s.toLowerCase(); |
|||
|
|||
StringBuilder sb = new StringBuilder(s.length()); |
|||
boolean upperCase = false; |
|||
for (int i = 0; i < s.length(); i++) { |
|||
char c = s.charAt(i); |
|||
|
|||
if (c == SEPARATOR) { |
|||
upperCase = true; |
|||
} else if (upperCase) { |
|||
sb.append(Character.toUpperCase(c)); |
|||
upperCase = false; |
|||
} else { |
|||
sb.append(c); |
|||
} |
|||
} |
|||
|
|||
return sb.toString(); |
|||
} |
|||
|
|||
/** |
|||
* 驼峰命名法工具 |
|||
* |
|||
* @return toCamelCase(" hello_world ") == "helloWorld" |
|||
* toCapitalizeCamelCase("hello_world") == "HelloWorld" |
|||
* toUnderScoreCase("helloWorld") = "hello_world" |
|||
*/ |
|||
public static String toCapitalizeCamelCase(String s) { |
|||
if (s == null) { |
|||
return null; |
|||
} |
|||
s = toCamelCase(s); |
|||
return s.substring(0, 1).toUpperCase() + s.substring(1); |
|||
} |
|||
|
|||
/** |
|||
* 驼峰命名法工具 |
|||
* |
|||
* @return toCamelCase(" hello_world ") == "helloWorld" |
|||
* toCapitalizeCamelCase("hello_world") == "HelloWorld" |
|||
* toUnderScoreCase("helloWorld") = "hello_world" |
|||
*/ |
|||
static String toUnderScoreCase(String s) { |
|||
if (s == null) { |
|||
return null; |
|||
} |
|||
|
|||
StringBuilder sb = new StringBuilder(); |
|||
boolean upperCase = false; |
|||
for (int i = 0; i < s.length(); i++) { |
|||
char c = s.charAt(i); |
|||
|
|||
boolean nextUpperCase = true; |
|||
|
|||
if (i < (s.length() - 1)) { |
|||
nextUpperCase = Character.isUpperCase(s.charAt(i + 1)); |
|||
} |
|||
|
|||
if ((i > 0) && Character.isUpperCase(c)) { |
|||
if (!upperCase || !nextUpperCase) { |
|||
sb.append(SEPARATOR); |
|||
} |
|||
upperCase = true; |
|||
} else { |
|||
upperCase = false; |
|||
} |
|||
|
|||
sb.append(Character.toLowerCase(c)); |
|||
} |
|||
|
|||
return sb.toString(); |
|||
} |
|||
|
|||
/** |
|||
* 获取ip地址 |
|||
*/ |
|||
public static String getIp(HttpServletRequest request) { |
|||
String ip = request.getHeader("x-forwarded-for"); |
|||
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { |
|||
ip = request.getHeader("Proxy-Client-IP"); |
|||
} |
|||
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { |
|||
ip = request.getHeader("WL-Proxy-Client-IP"); |
|||
} |
|||
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { |
|||
ip = request.getRemoteAddr(); |
|||
} |
|||
String comma = ","; |
|||
String localhost = "127.0.0.1"; |
|||
if (ip.contains(comma)) { |
|||
ip = ip.split(",")[0]; |
|||
} |
|||
if (localhost.equals(ip)) { |
|||
// 获取本机真正的ip地址 |
|||
try { |
|||
ip = InetAddress.getLocalHost().getHostAddress(); |
|||
} catch (UnknownHostException e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
} |
|||
return ip; |
|||
} |
|||
|
|||
/** |
|||
* 根据ip获取详细地址 |
|||
*/ |
|||
public static String getCityInfo(String ip) { |
|||
if (ipLocal) { |
|||
return getLocalCityInfo(ip); |
|||
} else { |
|||
return getHttpCityInfo(ip); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据ip获取详细地址 |
|||
*/ |
|||
public static String getHttpCityInfo(String ip) { |
|||
String api = String.format(ElAdminConstant.Url.IP_URL, ip); |
|||
JSONObject object = JSONUtil.parseObj(HttpUtil.get(api)); |
|||
return object.get("addr", String.class); |
|||
} |
|||
|
|||
/** |
|||
* 根据ip获取详细地址 |
|||
*/ |
|||
public static String getLocalCityInfo(String ip) { |
|||
try { |
|||
DataBlock dataBlock = new DbSearcher(config, file.getPath()) |
|||
.binarySearch(ip); |
|||
String region = dataBlock.getRegion(); |
|||
String address = region.replace("0|", ""); |
|||
char symbol = '|'; |
|||
if (address.charAt(address.length() - 1) == symbol) { |
|||
address = address.substring(0, address.length() - 1); |
|||
} |
|||
return address.equals(ElAdminConstant.REGION) ? "内网IP" : address; |
|||
} catch (Exception e) { |
|||
log.error(e.getMessage(), e); |
|||
} |
|||
return ""; |
|||
} |
|||
|
|||
public static String getBrowser(HttpServletRequest request) { |
|||
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent")); |
|||
Browser browser = userAgent.getBrowser(); |
|||
return browser.getName(); |
|||
} |
|||
|
|||
/** |
|||
* 获得当天是周几 |
|||
*/ |
|||
public static String getWeekDay() { |
|||
String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; |
|||
Calendar cal = Calendar.getInstance(); |
|||
cal.setTime(new Date()); |
|||
|
|||
int w = cal.get(Calendar.DAY_OF_WEEK) - 1; |
|||
if (w < 0) { |
|||
w = 0; |
|||
} |
|||
return weekDays[w]; |
|||
} |
|||
|
|||
/** |
|||
* 获取当前机器的IP |
|||
* |
|||
* @return / |
|||
*/ |
|||
public static String getLocalIp() { |
|||
try { |
|||
InetAddress candidateAddress = null; |
|||
// 遍历所有的网络接口 |
|||
for (Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); interfaces.hasMoreElements();) { |
|||
NetworkInterface anInterface = interfaces.nextElement(); |
|||
// 在所有的接口下再遍历IP |
|||
for (Enumeration<InetAddress> inetAddresses = anInterface.getInetAddresses(); inetAddresses.hasMoreElements();) { |
|||
InetAddress inetAddr = inetAddresses.nextElement(); |
|||
// 排除loopback类型地址 |
|||
if (!inetAddr.isLoopbackAddress()) { |
|||
if (inetAddr.isSiteLocalAddress()) { |
|||
// 如果是site-local地址,就是它了 |
|||
return inetAddr.getHostAddress(); |
|||
} else if (candidateAddress == null) { |
|||
// site-local类型的地址未被发现,先记录候选地址 |
|||
candidateAddress = inetAddr; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (candidateAddress != null) { |
|||
return candidateAddress.getHostAddress(); |
|||
} |
|||
// 如果没有发现 non-loopback地址.只能用最次选的方案 |
|||
InetAddress jdkSuppliedAddress = InetAddress.getLocalHost(); |
|||
if (jdkSuppliedAddress == null) { |
|||
return ""; |
|||
} |
|||
return jdkSuppliedAddress.getHostAddress(); |
|||
} catch (Exception e) { |
|||
return ""; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,305 @@ |
|||
package com.canvas.web.utils; |
|||
|
|||
import com.canvas.web.config.UploadFileConfig; |
|||
import org.apache.commons.fileupload.FileItem; |
|||
import org.apache.commons.fileupload.FileUploadException; |
|||
import org.apache.commons.fileupload.disk.DiskFileItemFactory; |
|||
import org.apache.commons.fileupload.servlet.ServletFileUpload; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.io.File; |
|||
import java.io.IOException; |
|||
import java.text.SimpleDateFormat; |
|||
import java.util.*; |
|||
|
|||
public class UploadUtils { |
|||
|
|||
// 表单字段常量 |
|||
public static final String FORM_FIELDS = "form_fields"; |
|||
|
|||
// 文件域常量 |
|||
public static final String FILE_FIELDS = "file"; |
|||
// 定义允许上传的文件扩展名 |
|||
private Map<String, String> extMap = new HashMap<String, String>(); |
|||
// 文件保存目录路径 |
|||
private String uploadPath = UploadFileConfig.uploadFolder; |
|||
// 文件的目录名 |
|||
private String dirName = "images"; |
|||
// 上传临时路径 |
|||
private static final String TEMP_PATH = "temp"; |
|||
// 临时存相对路径 |
|||
private String tempPath = uploadPath + TEMP_PATH; |
|||
// 单个文件最大上传大小(10M) |
|||
private long fileMaxSize = 1024 * 1024 * 10; |
|||
// 最大文件大小(100M) |
|||
private long maxSize = 1024 * 1024 * 100; |
|||
// 文件保存目录url |
|||
private String saveUrl; |
|||
// 文件最终的url包括文件名 |
|||
private List<String> fileUrl = new ArrayList<>(); |
|||
|
|||
/** |
|||
* 构造函数 |
|||
*/ |
|||
public UploadUtils() { |
|||
// 其中images,flashs,medias,files,对应文件夹名称,对应dirName |
|||
// key文件夹名称 |
|||
// value该文件夹内可以上传文件的后缀名 |
|||
extMap.put("images", "gif,jpg,jpeg,png,bmp"); |
|||
extMap.put("flashs", "swf,flv"); |
|||
extMap.put("medias", "swf,flv,mp3,wav,wma,wmv,mid,avi,mpg,asf,rm,rmvb"); |
|||
extMap.put("files", "doc,docx,xls,xlsx,ppt,htm,html,txt,zip,rar,gz,bz2"); |
|||
} |
|||
|
|||
/** |
|||
* 文件上传 |
|||
* |
|||
* @param request |
|||
* @return |
|||
*/ |
|||
@SuppressWarnings("unchecked") |
|||
public Map<String, Object> uploadFile(HttpServletRequest request, String name) { |
|||
// 验证文件并返回错误信息 |
|||
String error = this.validateFields(request, name); |
|||
// 初始化表单元素 |
|||
Map<String, Object> fieldsMap = new HashMap<String, Object>(); |
|||
if (error.equals("")) { |
|||
fieldsMap = this.initFields(request); |
|||
} |
|||
List<FileItem> fiList = (List<FileItem>) fieldsMap.get(UploadUtils.FILE_FIELDS); |
|||
if (fiList != null) { |
|||
for (FileItem item : fiList) { |
|||
// 上传文件并返回错误信息 |
|||
error = this.saveFile(item); |
|||
} |
|||
} |
|||
// 返回结果 |
|||
Map<String, Object> result = new HashMap<>(); |
|||
result.put("error", error); |
|||
result.put("image", this.fileUrl); |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 上传验证并初始化目录 |
|||
* |
|||
* @param request |
|||
* @return |
|||
*/ |
|||
private String validateFields(HttpServletRequest request, String name) { |
|||
String errorInfo = ""; |
|||
// 获取内容类型 |
|||
String contentType = request.getContentType(); |
|||
int contentLength = request.getContentLength(); |
|||
// 初始化上传路径,不存在则创建 |
|||
File uploadDir = new File(uploadPath); |
|||
// 目录不存在则创建 |
|||
if (!uploadDir.exists()) { |
|||
uploadDir.mkdirs(); |
|||
} |
|||
if (contentType == null || !contentType.startsWith("multipart")) { |
|||
// TODO |
|||
System.out.println("请求不包含multipart/form-data流"); |
|||
errorInfo = "请求不包含multipart/form-data流"; |
|||
} else if (maxSize < contentLength) { |
|||
// TODO |
|||
System.out.println("上传文件大小超出文件最大大小"); |
|||
errorInfo = "上传文件大小超出文件最大大小[" + maxSize + "]"; |
|||
} else if (!ServletFileUpload.isMultipartContent(request)) { |
|||
// TODO |
|||
errorInfo = "请选择文件"; |
|||
} else if (!uploadDir.isDirectory()) { |
|||
// TODO |
|||
errorInfo = "上传目录[" + uploadPath + "]不存在"; |
|||
} else if (!uploadDir.canWrite()) { |
|||
// TODO |
|||
errorInfo = "上传目录[" + uploadPath + "]没有写权限"; |
|||
} else if (!extMap.containsKey(dirName)) { |
|||
// TODO |
|||
errorInfo = "目录名不正确"; |
|||
} else { |
|||
// 上传路径 |
|||
uploadPath += dirName + "/" + name + "/"; |
|||
// 保存目录Url |
|||
saveUrl = dirName + "/" + name + "/"; |
|||
|
|||
// 创建一级目录 |
|||
File saveDirFile = new File(uploadPath); |
|||
if (!saveDirFile.exists()) { |
|||
saveDirFile.mkdirs(); |
|||
} |
|||
|
|||
// 创建二级目录(格式:年月日) |
|||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); |
|||
String ymd = sdf.format(new Date()); |
|||
uploadPath += ymd + "/"; |
|||
saveUrl += ymd + "/"; |
|||
File dirFile = new File(uploadPath); |
|||
if (!dirFile.exists()) { |
|||
dirFile.mkdirs(); |
|||
} |
|||
|
|||
// 创建上传临时目录 |
|||
File file = new File(tempPath); |
|||
if (!file.exists()) { |
|||
file.mkdirs(); |
|||
} |
|||
} |
|||
return errorInfo; |
|||
} |
|||
|
|||
/** |
|||
* 处理上传内容 |
|||
* |
|||
* @return |
|||
*/ |
|||
// @SuppressWarnings("unchecked") |
|||
private Map<String, Object> initFields(HttpServletRequest request) { |
|||
// 存储表单字段和非表单字段 |
|||
Map<String, Object> map = new HashMap<String, Object>(); |
|||
// 第一步:判断request |
|||
boolean isMultipart = ServletFileUpload.isMultipartContent(request); |
|||
// 第二步:解析request |
|||
if (isMultipart) { |
|||
// 设置环境:创建一个DiskFileItemFactory工厂 |
|||
DiskFileItemFactory factory = new DiskFileItemFactory(); |
|||
// 阀值,超过这个值才会写到临时目录,否则在内存中 |
|||
factory.setSizeThreshold(1024 * 1024 * 10); |
|||
// 设置上传文件的临时目录 |
|||
factory.setRepository(new File(tempPath)); |
|||
// 核心操作类:创建一个文件上传解析器。 |
|||
ServletFileUpload upload = new ServletFileUpload(factory); |
|||
// 设置文件名称编码(解决上传"文件名"的中文乱码) |
|||
upload.setHeaderEncoding("UTF-8"); |
|||
// 限制单个文件上传大小 |
|||
upload.setFileSizeMax(fileMaxSize); |
|||
// 限制总上传文件大小 |
|||
upload.setSizeMax(maxSize); |
|||
// 使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项 |
|||
List<FileItem> items = null; |
|||
try { |
|||
items = upload.parseRequest(request); |
|||
} catch (FileUploadException e) { |
|||
// TODO Auto-generated catch block |
|||
e.printStackTrace(); |
|||
} |
|||
|
|||
// 第3步:处理uploaded items |
|||
if (items != null && items.size() > 0) { |
|||
Iterator<FileItem> iter = items.iterator(); |
|||
// 文件域对象 |
|||
List<FileItem> list = new ArrayList<FileItem>(); |
|||
// 表单字段 |
|||
Map<String, String> fields = new HashMap<String, String>(); |
|||
while (iter.hasNext()) { |
|||
FileItem item = iter.next(); |
|||
// 处理所有表单元素和文件域表单元素 |
|||
if (item.isFormField()) { |
|||
// 如果fileitem中封装的是普通输入项的数据(输出名、值) |
|||
String name = item.getFieldName();// 普通输入项数据的名 |
|||
String value = item.getString(); |
|||
fields.put(name, value); |
|||
} else { |
|||
//如果fileitem中封装的是上传文件,得到上传的文件名称 |
|||
// 文件域表单元素 |
|||
list.add(item); |
|||
} |
|||
} |
|||
map.put(FORM_FIELDS, fields); |
|||
map.put(FILE_FIELDS, list); |
|||
} |
|||
} |
|||
return map; |
|||
} |
|||
|
|||
/** |
|||
* 保存文件 |
|||
* |
|||
* @param item |
|||
* @return |
|||
*/ |
|||
private String saveFile(FileItem item) { |
|||
String error = ""; |
|||
String fileName = item.getName(); |
|||
String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(); |
|||
|
|||
if (item.getSize() > maxSize) { // 检查文件大小 |
|||
// TODO |
|||
error = "上传文件大小超过限制"; |
|||
} else if (!Arrays.<String>asList(extMap.get(dirName).split(",")).contains(fileExt)) {// 检查扩展名 |
|||
error = "上传文件扩展名是不允许的扩展名。\n只允许" + extMap.get(dirName) + "格式。"; |
|||
} else { |
|||
// 存储文件重命名 |
|||
SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss"); |
|||
String newFileName = df.format(new Date()) + new Random().nextInt(1000) + "." + fileExt; |
|||
|
|||
// 新增值文件数组 |
|||
String filePath = saveUrl + newFileName; |
|||
fileUrl.add(filePath); |
|||
|
|||
// 写入文件 |
|||
try { |
|||
File uploadedFile = new File(uploadPath, newFileName); |
|||
item.write(uploadedFile); |
|||
} catch (IOException e) { |
|||
e.printStackTrace(); |
|||
System.out.println("上传失败了!!!"); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
return error; |
|||
} |
|||
|
|||
/** |
|||
* *********************get/set方法********************************* |
|||
*/ |
|||
public String getSaveUrl() { |
|||
return saveUrl; |
|||
} |
|||
|
|||
public String getUploadPath() { |
|||
return uploadPath; |
|||
} |
|||
|
|||
public long getMaxSize() { |
|||
return maxSize; |
|||
} |
|||
|
|||
public void setMaxSize(long maxSize) { |
|||
this.maxSize = maxSize; |
|||
} |
|||
|
|||
public Map<String, String> getExtMap() { |
|||
return extMap; |
|||
} |
|||
|
|||
public void setExtMap(Map<String, String> extMap) { |
|||
this.extMap = extMap; |
|||
} |
|||
|
|||
public String getDirName() { |
|||
return dirName; |
|||
} |
|||
|
|||
public void setDirName(String dirName) { |
|||
this.dirName = dirName; |
|||
} |
|||
|
|||
public String getTempPath() { |
|||
return tempPath; |
|||
} |
|||
|
|||
public void setTempPath(String tempPath) { |
|||
this.tempPath = tempPath; |
|||
} |
|||
|
|||
public List getFileUrl() { |
|||
return fileUrl; |
|||
} |
|||
|
|||
public void setFileUrl(List fileUrl) { |
|||
this.fileUrl = fileUrl; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,45 @@ |
|||
import com.canvas.web.annotation.rest.AnonymousGetMapping; |
|||
import com.canvas.web.utils.SpringContextHolder; |
|||
import io.swagger.annotations.Api; |
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; |
|||
import org.springframework.boot.web.servlet.server.ServletWebServerFactory; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing; |
|||
import org.springframework.scheduling.annotation.EnableAsync; |
|||
import org.springframework.transaction.annotation.EnableTransactionManagement; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
@EnableAsync |
|||
@RestController |
|||
@Api(hidden = true) |
|||
@SpringBootApplication |
|||
@EnableTransactionManagement |
|||
@EnableJpaAuditing(auditorAwareRef = "auditorAware") |
|||
public class AppRun { |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(AppRun.class, args); |
|||
} |
|||
|
|||
@Bean |
|||
public SpringContextHolder springContextHolder(){ |
|||
return new SpringContextHolder(); |
|||
} |
|||
|
|||
public ServletWebServerFactory webServerFactory(){ |
|||
TomcatServletWebServerFactory fa=new TomcatServletWebServerFactory(); |
|||
fa.addConnectorCustomizers(connector -> connector.setProperty("relaxedQueryChars","[]{}")); |
|||
return fa; |
|||
} |
|||
|
|||
/** |
|||
* 访问首页提示 |
|||
* @return |
|||
*/ |
|||
@AnonymousGetMapping("/") |
|||
public String index(){ |
|||
return "Backend service started successfully"; |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
package modules.security.config; |
|||
|
|||
|
|||
import modules.security.config.bean.LoginProperties; |
|||
import modules.security.config.bean.SecurityProperties; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
|
|||
@Configuration |
|||
public class ConfigBeanConfiguration { |
|||
|
|||
@Bean |
|||
@ConfigurationProperties(prefix="login",ignoreUnknownFields=true) |
|||
public LoginProperties loginProperties(){ |
|||
return new LoginProperties(); |
|||
} |
|||
|
|||
@Bean |
|||
@ConfigurationProperties(prefix = "jwt",ignoreUnknownFields = true) |
|||
public SecurityProperties securityProperties(){ |
|||
return new SecurityProperties(); |
|||
} |
|||
} |
@ -0,0 +1,31 @@ |
|||
package modules.security.config; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import modules.security.security.JwtAccessDeniedHandler; |
|||
import modules.security.security.JwtAuthenticationEntryPoint; |
|||
import modules.security.security.SecurityProperties; |
|||
import modules.security.security.TokenProvider; |
|||
import modules.security.service.UserCacheClean; |
|||
import org.springframework.context.ApplicationContext; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; |
|||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
|||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; |
|||
import org.springframework.web.filter.CorsFilter; |
|||
|
|||
|
|||
@Configuration |
|||
@EnableWebSecurity |
|||
@RequiredArgsConstructor |
|||
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true) |
|||
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { |
|||
|
|||
private final TokenProvider tokenProvider; |
|||
|
|||
private final CorsFilter corsFilter; |
|||
private final JwtAuthenticationEntryPoint authenticationErrorHandler; |
|||
private final JwtAccessDeniedHandler jwtAccessDeniedHandler; |
|||
private final ApplicationContext applicationContext; |
|||
private final SecurityProperties properties; |
|||
private final UserCacheClean userCacheClean; |
|||
} |
@ -0,0 +1,48 @@ |
|||
package modules.security.config.bean; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class LoginCode { |
|||
|
|||
/** |
|||
* 验证码配置 |
|||
*/ |
|||
private LoginCodeEnum codeType; |
|||
|
|||
|
|||
/** |
|||
* 验证码有效期 分钟 |
|||
*/ |
|||
private Long expiration = 2L; |
|||
|
|||
/** |
|||
* 验证码内容长度 |
|||
*/ |
|||
private int length = 2; |
|||
|
|||
/** |
|||
* 验证码宽度 |
|||
*/ |
|||
private int width = 111; |
|||
|
|||
/** |
|||
* 验证码高度 |
|||
*/ |
|||
private int height = 36; |
|||
|
|||
/** |
|||
* 验证码字体 |
|||
*/ |
|||
private String fontName; |
|||
|
|||
/** |
|||
* 字体大小 |
|||
*/ |
|||
private int fontSize = 25; |
|||
|
|||
|
|||
public LoginCodeEnum getCodeType() { |
|||
return codeType; |
|||
} |
|||
} |
@ -0,0 +1,25 @@ |
|||
package modules.security.config.bean; |
|||
|
|||
//验证码配置枚举 |
|||
public enum LoginCodeEnum { |
|||
/** |
|||
* 算术 |
|||
*/ |
|||
arithmetic, |
|||
|
|||
/** |
|||
* 中文 |
|||
*/ |
|||
chinese, |
|||
|
|||
/** |
|||
* 中文闪图 |
|||
*/ |
|||
chinese_gif, |
|||
|
|||
/** |
|||
* 闪图 |
|||
*/ |
|||
gif, |
|||
spec |
|||
} |
@ -0,0 +1,91 @@ |
|||
package modules.security.config.bean; |
|||
|
|||
import com.canvas.web.exception.BaseException; |
|||
import com.canvas.web.exception.user.UserException; |
|||
import com.canvas.web.utils.StringUtils; |
|||
import com.wf.captcha.*; |
|||
import com.wf.captcha.base.Captcha; |
|||
import lombok.Data; |
|||
|
|||
import java.awt.*; |
|||
import java.util.Objects; |
|||
|
|||
@Data |
|||
public class LoginProperties { |
|||
|
|||
/** |
|||
* 账号单用户 登录 |
|||
*/ |
|||
private boolean singleLogin = false; |
|||
|
|||
|
|||
private LoginCode loginCode; |
|||
|
|||
|
|||
/** |
|||
* 用户登录信息缓存 |
|||
*/ |
|||
private boolean cacheEnable; |
|||
|
|||
|
|||
public boolean isSingleLogin() { |
|||
return singleLogin; |
|||
} |
|||
|
|||
public boolean isCacheEnable() { |
|||
return cacheEnable; |
|||
} |
|||
|
|||
/** |
|||
* 获取验证码生产类 |
|||
* @return |
|||
*/ |
|||
public Captcha getCaptcha(){ |
|||
if (Objects.isNull(loginCode)){ |
|||
loginCode=new LoginCode(); |
|||
if (Objects.isNull(loginCode.getCodeType())){ |
|||
loginCode.setCodeType(LoginCodeEnum.arithmetic); |
|||
} |
|||
} |
|||
return switchCaptcha(loginCode); |
|||
} |
|||
|
|||
/** |
|||
* 依据配置信息生产验证码 |
|||
* @param loginCode |
|||
* @return |
|||
*/ |
|||
private Captcha switchCaptcha(LoginCode loginCode){ |
|||
Captcha captcha; |
|||
synchronized (this){ |
|||
switch (loginCode.getCodeType()){ |
|||
case arithmetic: |
|||
captcha=new ArithmeticCaptcha(loginCode.getWidth(),loginCode.getHeight()); |
|||
|
|||
//几位数运算,默认是两位 |
|||
captcha.setLen(loginCode.getLength()); |
|||
break; |
|||
case chinese: |
|||
captcha=new ChineseCaptcha(loginCode.getWidth(),loginCode.getHeight()); |
|||
captcha.setLen(loginCode.getLength()); |
|||
case chinese_gif: |
|||
captcha=new ChineseGifCaptcha(loginCode.getWidth(),loginCode.getHeight()); |
|||
captcha.setLen(loginCode.getLength()); |
|||
break; |
|||
case gif: |
|||
captcha=new GifCaptcha(loginCode.getWidth(),loginCode.getHeight()); |
|||
captcha.setLen(loginCode.getLength()); |
|||
case spec: |
|||
captcha=new SpecCaptcha(loginCode.getWidth(),loginCode.getHeight()); |
|||
captcha.setLen(loginCode.getLength()); |
|||
break; |
|||
default: |
|||
throw new BaseException("验证码配置信息错误!正确配置查看 LoginCodeEnum"); |
|||
} |
|||
} |
|||
if (StringUtils.isNotBlank(loginCode.getFontName())){ |
|||
captcha.setFont(new Font(loginCode.getFontName(), Font.PLAIN, loginCode.getFontSize())); |
|||
} |
|||
return captcha; |
|||
} |
|||
} |
@ -0,0 +1,52 @@ |
|||
package modules.security.config.bean; |
|||
|
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class SecurityProperties { |
|||
|
|||
/** |
|||
* Request Headers:Authorization |
|||
*/ |
|||
private String header; |
|||
|
|||
/** |
|||
* 令牌前缀,最后留个空格 Bearer |
|||
*/ |
|||
private String tokenStartWith; |
|||
|
|||
/** |
|||
* 必须使用最少88位的Base64对该令牌进行编码 |
|||
*/ |
|||
private String base64Secret; |
|||
|
|||
/** |
|||
* 令牌过期时间此处单位/毫秒 |
|||
*/ |
|||
private Long tokenValidityInSeconds; |
|||
|
|||
/** |
|||
* 在线用户 key,根据 key 查询 redis 中在线用户的数据 |
|||
*/ |
|||
private String onlineKey; |
|||
|
|||
/** |
|||
* 验证码key |
|||
*/ |
|||
private String codeKey; |
|||
|
|||
/** |
|||
* token 续期检查 |
|||
*/ |
|||
private Long detect; |
|||
|
|||
/** |
|||
* 续期时间 |
|||
*/ |
|||
private Long renew; |
|||
|
|||
public String getTokenStartWith(){ |
|||
return tokenStartWith+" "; |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
package modules.security.security; |
|||
|
|||
import org.springframework.security.access.AccessDeniedException; |
|||
import org.springframework.security.web.access.AccessDeniedHandler; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.servlet.ServletException; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
|
|||
@Component |
|||
public class JwtAccessDeniedHandler implements AccessDeniedHandler { |
|||
@Override |
|||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { |
|||
//当前用户在没有权限的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应 |
|||
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
package modules.security.security; |
|||
|
|||
|
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.security.web.AuthenticationEntryPoint; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.servlet.ServletException; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
|
|||
@Component |
|||
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { |
|||
|
|||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException{ |
|||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException == null ? "Unauthorized" : authException.getMessage()); |
|||
} |
|||
} |
@ -0,0 +1,51 @@ |
|||
package modules.security.security; |
|||
|
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class SecurityProperties { |
|||
/** |
|||
* Request Headers:Authorization |
|||
*/ |
|||
private String header; |
|||
|
|||
/** |
|||
* 令牌前缀,最后留个空格 Bearer |
|||
*/ |
|||
private String tokenStartWith; |
|||
|
|||
/** |
|||
* 必须使用最少88位的Base64对该令牌进行编码 |
|||
*/ |
|||
private String base64Secret; |
|||
|
|||
/** |
|||
* 令牌过期时间此处单位/毫秒 |
|||
*/ |
|||
private Long tokenValidityInSeconds; |
|||
|
|||
/** |
|||
* 在线用户 key,根据 key 查询 redis 中在线用户的数据 |
|||
*/ |
|||
private String onlineKey; |
|||
|
|||
/** |
|||
* 验证码key |
|||
*/ |
|||
private String codeKey; |
|||
|
|||
/** |
|||
* token 续期检查 |
|||
*/ |
|||
private Long detect; |
|||
|
|||
/** |
|||
* 续期时间 |
|||
*/ |
|||
private Long renew; |
|||
|
|||
public String getTokenStartWith(){ |
|||
return tokenStartWith+" "; |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
package modules.security.security; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import modules.security.service.OnlineUserService; |
|||
import modules.security.service.UserCacheClean; |
|||
import org.springframework.security.config.annotation.SecurityConfigurerAdapter; |
|||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
|||
import org.springframework.security.web.DefaultSecurityFilterChain; |
|||
|
|||
@RequiredArgsConstructor |
|||
public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { |
|||
private final TokenProvider tokenProvider; |
|||
private final SecurityProperties properties; |
|||
private final OnlineUserService onlineUserService; |
|||
private final UserCacheClean userCacheClean; |
|||
} |
@ -0,0 +1,107 @@ |
|||
package modules.security.security; |
|||
|
|||
import cn.hutool.core.date.DateField; |
|||
import cn.hutool.core.date.DateUtil; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import com.canvas.web.utils.RedisUtils; |
|||
|
|||
import io.jsonwebtoken.*; |
|||
import io.jsonwebtoken.io.Decoders; |
|||
import io.jsonwebtoken.security.Keys; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.InitializingBean; |
|||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.userdetails.User; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.security.Key; |
|||
import java.util.ArrayList; |
|||
import java.util.Date; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
public class TokenProvider implements InitializingBean { |
|||
|
|||
private final SecurityProperties properties; |
|||
|
|||
private final RedisUtils redisUtils; |
|||
public static final String AUTHORITIES_KEY="auth"; |
|||
|
|||
private JwtParser jwtParser; |
|||
|
|||
private JwtBuilder jwtBuilder; |
|||
|
|||
public TokenProvider(SecurityProperties properties,RedisUtils redisUtils){ |
|||
this.properties=properties; |
|||
this.redisUtils=redisUtils; |
|||
} |
|||
|
|||
@Override |
|||
public void afterPropertiesSet() { |
|||
byte[] keyBytes = Decoders.BASE64.decode(properties.getBase64Secret()); |
|||
Key key = Keys.hmacShaKeyFor(keyBytes); |
|||
jwtParser = Jwts.parserBuilder() |
|||
.setSigningKey(key) |
|||
.build(); |
|||
jwtBuilder = Jwts.builder() |
|||
.signWith(key, SignatureAlgorithm.HS512); |
|||
} |
|||
|
|||
/** |
|||
* 创建Token 设置永不过期, |
|||
* Token 的时间有效性转到Redis 维护 |
|||
* @param authentication |
|||
* @return |
|||
*/ |
|||
public String createToken(Authentication authentication) { |
|||
return jwtBuilder |
|||
// 加入ID确保生成的 Token 都不一致 |
|||
.setId(IdUtil.simpleUUID()) |
|||
.setSubject(authentication.getName()) |
|||
.compact(); |
|||
} |
|||
|
|||
/** |
|||
* 依据Token 获取鉴权信息 |
|||
* @param token |
|||
* @return |
|||
*/ |
|||
Authentication getAuthentication(String token){ |
|||
Claims claims=getClaims(token); |
|||
User principal = new User(claims.getSubject(), "******", new ArrayList<>()); |
|||
return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>()); |
|||
} |
|||
|
|||
public Claims getClaims(String token) { |
|||
return jwtParser |
|||
.parseClaimsJws(token) |
|||
.getBody(); |
|||
} |
|||
|
|||
/** |
|||
* @param token 需要检查的token |
|||
*/ |
|||
public void checkRenewal(String token) { |
|||
// 判断是否续期token,计算token的过期时间 |
|||
long time = redisUtils.getExpire(properties.getOnlineKey() + token) * 1000; |
|||
Date expireDate = DateUtil.offset(new Date(), DateField.MILLISECOND, (int) time); |
|||
// 判断当前时间与过期时间的时间差 |
|||
long differ = expireDate.getTime() - System.currentTimeMillis(); |
|||
// 如果在续期检查的范围内,则续期 |
|||
if (differ <= properties.getDetect()) { |
|||
long renew = time + properties.getRenew(); |
|||
redisUtils.expire(properties.getOnlineKey() + token, renew, TimeUnit.MILLISECONDS); |
|||
} |
|||
} |
|||
|
|||
public String getToken(HttpServletRequest request) { |
|||
final String requestHeader = request.getHeader(properties.getHeader()); |
|||
if (requestHeader != null && requestHeader.startsWith(properties.getTokenStartWith())) { |
|||
return requestHeader.substring(7); |
|||
} |
|||
return null; |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
package modules.security.service; |
|||
|
|||
import com.canvas.web.utils.RedisUtils; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import modules.security.config.bean.SecurityProperties; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
|
|||
@Service |
|||
@Slf4j |
|||
public class OnlineUserService { |
|||
private final SecurityProperties properties; |
|||
private final RedisUtils redisUtils; |
|||
|
|||
public OnlineUserService(SecurityProperties properties, RedisUtils redisUtils) { |
|||
this.properties = properties; |
|||
this.redisUtils = redisUtils; |
|||
} |
|||
} |
@ -0,0 +1,31 @@ |
|||
package modules.security.service; |
|||
|
|||
|
|||
|
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
//用于清理用户登录信息缓存 |
|||
@Component |
|||
public class UserCacheClean { |
|||
|
|||
/** |
|||
* 清理特定用户缓存信息 |
|||
* 用户信息变更时 |
|||
* |
|||
* @param userName |
|||
*/ |
|||
public void cleanUserCache(String userName) { |
|||
if (StringUtils.isNotEmpty(userName)) { |
|||
UserDetailsServiceImpl.userDtoCache.remove(userName); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清理所有用户的缓存信息 |
|||
* ,如发生角色授权信息变化,可以简便的全部失效缓存 |
|||
*/ |
|||
public void cleanAll() { |
|||
UserDetailsServiceImpl.userDtoCache.clear(); |
|||
} |
|||
} |
@ -0,0 +1,74 @@ |
|||
package modules.security.service; |
|||
|
|||
import com.canvas.web.exception.BaseException; |
|||
import lombok.RequiredArgsConstructor; |
|||
import modules.security.config.bean.LoginProperties; |
|||
import modules.security.service.dto.JwtUserDto; |
|||
import modules.system.service.DataService; |
|||
import modules.system.service.RoleService; |
|||
import modules.system.service.UserService; |
|||
|
|||
import modules.system.service.dto.UserDto; |
|||
import org.springframework.security.core.userdetails.UserDetailsService; |
|||
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
|
|||
import javax.persistence.EntityNotFoundException; |
|||
import java.util.*; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
|
|||
@RequiredArgsConstructor |
|||
@Service("userDetailsService") |
|||
public class UserDetailsServiceImpl implements UserDetailsService { |
|||
|
|||
private final UserService userService; |
|||
private final RoleService roleService; |
|||
private final DataService dataService; |
|||
private final LoginProperties loginProperties; |
|||
|
|||
public void setEnableCache(boolean enableCache) { |
|||
this.loginProperties.setCacheEnable(enableCache); |
|||
} |
|||
|
|||
/** |
|||
* 用户信息缓存 |
|||
* |
|||
* |
|||
*/ |
|||
static Map<String, JwtUserDto> userDtoCache = new ConcurrentHashMap<>(); |
|||
|
|||
|
|||
@Override |
|||
public JwtUserDto loadUserByUsername(String username) { |
|||
boolean searchDb = true; |
|||
JwtUserDto jwtUserDto = null; |
|||
if (loginProperties.isCacheEnable() && userDtoCache.containsKey(username)) { |
|||
jwtUserDto = userDtoCache.get(username); |
|||
searchDb = false; |
|||
} |
|||
if (searchDb) { |
|||
UserDto user; |
|||
try { |
|||
user = userService.findByName(username); |
|||
} catch (EntityNotFoundException e) { |
|||
// SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsException |
|||
throw new UsernameNotFoundException("", e); |
|||
} |
|||
if (user == null) { |
|||
throw new UsernameNotFoundException(""); |
|||
} else { |
|||
if (!user.getEnabled()) { |
|||
throw new BaseException("账号未激活!"); |
|||
} |
|||
jwtUserDto = new JwtUserDto( |
|||
user, |
|||
dataService.getDeptIds(user), |
|||
roleService.mapToGrantedAuthorities(user) |
|||
); |
|||
userDtoCache.put(username, jwtUserDto); |
|||
} |
|||
} |
|||
return jwtUserDto; |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
package modules.security.service.dto; |
|||
|
|||
|
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
|
|||
import javax.validation.constraints.NotBlank; |
|||
|
|||
@Getter |
|||
@Setter |
|||
public class AuthUserDto { |
|||
|
|||
@NotBlank |
|||
private String username; |
|||
|
|||
@NotBlank |
|||
private String password; |
|||
|
|||
private String code; |
|||
|
|||
private String uuid = ""; |
|||
} |
@ -0,0 +1,65 @@ |
|||
package modules.security.service.dto; |
|||
|
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Getter; |
|||
import modules.system.service.dto.UserDto; |
|||
import org.springframework.security.core.GrantedAuthority; |
|||
import org.springframework.security.core.userdetails.UserDetails; |
|||
|
|||
import java.util.List; |
|||
import java.util.Set; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Getter |
|||
@AllArgsConstructor |
|||
public class JwtUserDto implements UserDetails { |
|||
|
|||
private final UserDto user; |
|||
|
|||
private final List<Long> dataScopes; |
|||
|
|||
@JsonIgnore |
|||
private final List<GrantedAuthority> authorities; |
|||
|
|||
public Set<String> getRoles() { |
|||
return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); |
|||
} |
|||
|
|||
@Override |
|||
@JsonIgnore |
|||
public String getPassword() { |
|||
return user.getPassword(); |
|||
} |
|||
|
|||
@Override |
|||
@JsonIgnore |
|||
public String getUsername() { |
|||
return user.getUsername(); |
|||
} |
|||
|
|||
@JsonIgnore |
|||
@Override |
|||
public boolean isAccountNonExpired() { |
|||
return true; |
|||
} |
|||
|
|||
@JsonIgnore |
|||
@Override |
|||
public boolean isAccountNonLocked() { |
|||
return true; |
|||
} |
|||
|
|||
@JsonIgnore |
|||
@Override |
|||
public boolean isCredentialsNonExpired() { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
@JsonIgnore |
|||
public boolean isEnabled() { |
|||
return user.getEnabled(); |
|||
} |
|||
} |
@ -0,0 +1,15 @@ |
|||
package modules.system.service; |
|||
|
|||
import modules.system.service.dto.UserDto; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface DataService { |
|||
|
|||
/** |
|||
* 获取数据权限 |
|||
* @param user / |
|||
* @return / |
|||
*/ |
|||
List<Long> getDeptIds(UserDto user); |
|||
} |
@ -0,0 +1,16 @@ |
|||
package modules.system.service; |
|||
|
|||
import modules.system.service.dto.UserDto; |
|||
import org.springframework.security.core.GrantedAuthority; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface RoleService { |
|||
|
|||
/** |
|||
* 获取用户权限信息 |
|||
* @param user 用户信息 |
|||
* @return 权限信息 |
|||
*/ |
|||
List<GrantedAuthority> mapToGrantedAuthorities(UserDto user); |
|||
} |
@ -0,0 +1,98 @@ |
|||
package modules.system.service; |
|||
|
|||
import modules.system.service.dto.UserDto; |
|||
import modules.system.service.dto.UserQueryCriteria; |
|||
import org.springframework.data.domain.Pageable; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
|
|||
public interface UserService { |
|||
|
|||
/** |
|||
* 根据ID查询 |
|||
* @param id ID |
|||
* @return / |
|||
*/ |
|||
UserDto findById(long id); |
|||
|
|||
/** |
|||
* 新增用户 |
|||
* @param resources / |
|||
*/ |
|||
// void create(User resources); |
|||
|
|||
/** |
|||
* 编辑用户 |
|||
* @param resources / |
|||
*/ |
|||
// void update(User resources); |
|||
|
|||
/** |
|||
* 删除用户 |
|||
* @param ids / |
|||
*/ |
|||
void delete(Set<Long> ids); |
|||
|
|||
/** |
|||
* 根据用户名查询 |
|||
* @param userName / |
|||
* @return / |
|||
*/ |
|||
UserDto findByName(String userName); |
|||
|
|||
|
|||
/** |
|||
* 修改密码 |
|||
* @param username 用户名 |
|||
* @param encryptPassword 密码 |
|||
*/ |
|||
void updatePass(String username, String encryptPassword); |
|||
|
|||
/** |
|||
* 修改头像 |
|||
* @param file 文件 |
|||
* @return / |
|||
*/ |
|||
Map<String, String> updateAvatar(MultipartFile file); |
|||
|
|||
/** |
|||
* 修改邮箱 |
|||
* @param username 用户名 |
|||
* @param email 邮箱 |
|||
*/ |
|||
void updateEmail(String username, String email); |
|||
|
|||
/** |
|||
* 查询全部 |
|||
* @param criteria 条件 |
|||
* @param pageable 分页参数 |
|||
* @return / |
|||
*/ |
|||
Object queryAll(UserQueryCriteria criteria, Pageable pageable); |
|||
|
|||
/** |
|||
* 查询全部不分页 |
|||
* @param criteria 条件 |
|||
* @return / |
|||
*/ |
|||
List<UserDto> queryAll(UserQueryCriteria criteria); |
|||
|
|||
/** |
|||
* 导出数据 |
|||
* @param queryAll 待导出的数据 |
|||
* @param response / |
|||
* @throws IOException / |
|||
*/ |
|||
void download(List<UserDto> queryAll, HttpServletResponse response) throws IOException; |
|||
|
|||
/** |
|||
* 用户自助修改资料 |
|||
* @param resources / |
|||
*/ |
|||
// void updateCenter(User resources); |
|||
} |
@ -0,0 +1,76 @@ |
|||
package modules.system.service.dto; |
|||
|
|||
import com.canvas.web.base.BaseDTO; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.List; |
|||
import java.util.Objects; |
|||
|
|||
|
|||
@Getter |
|||
@Setter |
|||
public class MenuDto extends BaseDTO implements Serializable { |
|||
|
|||
private Long id; |
|||
|
|||
private List<MenuDto> children; |
|||
|
|||
private Integer type; |
|||
|
|||
private String permission; |
|||
|
|||
private String title; |
|||
|
|||
private Integer menuSort; |
|||
|
|||
private String path; |
|||
|
|||
private String component; |
|||
|
|||
private Long pid; |
|||
|
|||
private Integer subCount; |
|||
|
|||
private Boolean iFrame; |
|||
|
|||
private Boolean cache; |
|||
|
|||
private Boolean hidden; |
|||
|
|||
private String componentName; |
|||
|
|||
private String icon; |
|||
|
|||
private Integer showPosition; |
|||
|
|||
public Boolean getHasChildren() { |
|||
return subCount > 0; |
|||
} |
|||
|
|||
public Boolean getLeaf() { |
|||
return subCount <= 0; |
|||
} |
|||
|
|||
public String getLabel() { |
|||
return title; |
|||
} |
|||
|
|||
@Override |
|||
public boolean equals(Object o) { |
|||
if (this == o) { |
|||
return true; |
|||
} |
|||
if (o == null || getClass() != o.getClass()) { |
|||
return false; |
|||
} |
|||
MenuDto menuDto = (MenuDto) o; |
|||
return Objects.equals(id, menuDto.id); |
|||
} |
|||
|
|||
@Override |
|||
public int hashCode() { |
|||
return Objects.hash(id); |
|||
} |
|||
} |
@ -0,0 +1,45 @@ |
|||
package modules.system.service.dto; |
|||
|
|||
import com.canvas.web.base.BaseDTO; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.Objects; |
|||
import java.util.Set; |
|||
|
|||
@Getter |
|||
@Setter |
|||
public class RoleDto extends BaseDTO implements Serializable { |
|||
|
|||
private Long id; |
|||
|
|||
private Set<MenuDto> menus; |
|||
|
|||
// private Set<DeptDto> depts; |
|||
|
|||
private String name; |
|||
|
|||
private String dataScope; |
|||
|
|||
private Integer level; |
|||
|
|||
private String description; |
|||
|
|||
@Override |
|||
public boolean equals(Object o) { |
|||
if (this == o) { |
|||
return true; |
|||
} |
|||
if (o == null || getClass() != o.getClass()) { |
|||
return false; |
|||
} |
|||
RoleDto roleDto = (RoleDto) o; |
|||
return Objects.equals(id, roleDto.id); |
|||
} |
|||
|
|||
@Override |
|||
public int hashCode() { |
|||
return Objects.hash(id); |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
package modules.system.service.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
@Data |
|||
public class RoleSmallDto implements Serializable { |
|||
private Long id; |
|||
|
|||
private String name; |
|||
|
|||
private Integer level; |
|||
|
|||
private String dataScope; |
|||
} |
@ -0,0 +1,50 @@ |
|||
package modules.system.service.dto; |
|||
|
|||
|
|||
import com.canvas.web.base.BaseDTO; |
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
import java.util.Set; |
|||
|
|||
@Getter |
|||
@Setter |
|||
public class UserDto extends BaseDTO implements Serializable { |
|||
|
|||
private Long id; |
|||
|
|||
private Set<RoleSmallDto> roles; |
|||
|
|||
//private Set<JobSmallDto> jobs; |
|||
|
|||
// private DeptSmallDto dept; |
|||
|
|||
private Long deptId; |
|||
|
|||
private String username; |
|||
|
|||
private String nickName; |
|||
|
|||
private String email; |
|||
|
|||
private String phone; |
|||
|
|||
private String gender; |
|||
|
|||
private String avatarName; |
|||
|
|||
private String avatarPath; |
|||
|
|||
@JsonIgnore |
|||
private String password; |
|||
|
|||
private Boolean enabled; |
|||
|
|||
@JsonIgnore |
|||
private Boolean isAdmin = false; |
|||
|
|||
private Date pwdResetTime; |
|||
} |
@ -0,0 +1,32 @@ |
|||
package modules.system.service.dto; |
|||
|
|||
import com.canvas.web.annotation.Query; |
|||
import lombok.Data; |
|||
|
|||
|
|||
import java.io.Serializable; |
|||
import java.sql.Timestamp; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
|
|||
@Data |
|||
public class UserQueryCriteria implements Serializable { |
|||
|
|||
@Query |
|||
private Long id; |
|||
|
|||
@Query(propName = "id", type = Query.Type.IN, joinName = "dept") |
|||
private Set<Long> deptIds = new HashSet<>(); |
|||
|
|||
@Query(blurry = "email,username,nickName") |
|||
private String blurry; |
|||
|
|||
@Query |
|||
private Boolean enabled; |
|||
|
|||
private Long deptId; |
|||
|
|||
@Query(type = Query.Type.BETWEEN) |
|||
private List<Timestamp> createTime; |
|||
} |
@ -0,0 +1,115 @@ |
|||
spring: |
|||
datasource: |
|||
druid: |
|||
db-type: com.alibaba.druid.pool.DruidDataSource |
|||
driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy |
|||
url: jdbc:log4jdbc:mysql://${DB_HOST:192.168.99.207}:${DB_PORT:3306}/${DB_NAME:yxkadmin}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false |
|||
username: ${DB_USER:root} |
|||
password: ${DB_PWD:ftzn83560792} |
|||
# 初始连接数 |
|||
initial-size: 5 |
|||
# 最小连接数 |
|||
min-idle: 10 |
|||
# 最大连接数 |
|||
max-active: 20 |
|||
# 获取连接超时时间 |
|||
max-wait: 5000 |
|||
# 连接有效性检测时间 |
|||
time-between-eviction-runs-millis: 60000 |
|||
# 连接在池中最小生存的时间 |
|||
min-evictable-idle-time-millis: 300000 |
|||
# 连接在池中最大生存的时间 |
|||
max-evictable-idle-time-millis: 900000 |
|||
test-while-idle: true |
|||
test-on-borrow: false |
|||
test-on-return: false |
|||
useGlobalDataSourceStat: true |
|||
# 检测连接是否有效 |
|||
validation-query: select 1 |
|||
# 配置监控统计 |
|||
webStatFilter: |
|||
enabled: true |
|||
stat-view-servlet: |
|||
enabled: true |
|||
url-pattern: /druid/* |
|||
reset-enable: false |
|||
login-username: admin |
|||
login-password: 123456 |
|||
filter: |
|||
stat: |
|||
enabled: true |
|||
# 记录慢SQL |
|||
log-slow-sql: true |
|||
slow-sql-millis: 1000 |
|||
merge-sql: true |
|||
wall: |
|||
config: |
|||
multi-statement-allow: true |
|||
|
|||
# 登录相关配置 |
|||
login: |
|||
# 登录缓存 |
|||
cache-enable: true |
|||
# 是否限制单用户登录 |
|||
single-login: false |
|||
# 验证码 |
|||
login-code: |
|||
# 验证码类型配置 查看 LoginProperties 类 |
|||
code-type: arithmetic |
|||
# 登录图形验证码有效时间/分钟 |
|||
expiration: 2 |
|||
# 验证码高度 |
|||
width: 111 |
|||
# 验证码宽度 |
|||
heigth: 36 |
|||
# 内容长度 |
|||
length: 2 |
|||
# 字体名称,为空则使用默认字体 |
|||
font-name: |
|||
# 字体大小 |
|||
font-size: 25 |
|||
|
|||
#jwt |
|||
jwt: |
|||
header: Authorization |
|||
# 令牌前缀 |
|||
token-start-with: Bearer |
|||
# 必须使用最少88位的Base64对该令牌进行编码 |
|||
base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI= |
|||
# 令牌过期时间 此处单位/毫秒 ,默认4小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html |
|||
token-validity-in-seconds: 14400000 |
|||
# 在线用户key |
|||
online-key: online-token- |
|||
# 验证码 |
|||
code-key: code-key- |
|||
# token 续期检查时间范围(默认30分钟,单位毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期 |
|||
detect: 1800000 |
|||
# 续期时间范围,默认1小时,单位毫秒 |
|||
renew: 3600000 |
|||
|
|||
#是否允许生成代码,生产环境设置为false |
|||
generator: |
|||
enabled: true |
|||
|
|||
#是否开启 swagger-ui |
|||
swagger: |
|||
enabled: true |
|||
|
|||
# IP 本地解析 |
|||
ip: |
|||
local-parsing: true |
|||
|
|||
# 文件存储路径 |
|||
file: |
|||
mac: |
|||
path: ~/file/ |
|||
avatar: ~/avatar/ |
|||
linux: |
|||
path: /home/canvasscreen/file/ |
|||
avatar: /home/canvasscreen/avatar/ |
|||
windows: |
|||
path: F:\code\canvasscreen\file\ |
|||
avatar: F:\code\canvasscreen\avatar\ |
|||
# 文件大小 /M |
|||
maxSize: 100 |
|||
avatarMaxSize: 5 |
@ -0,0 +1,60 @@ |
|||
server: |
|||
port: 7000 |
|||
|
|||
spring: |
|||
freemarker: |
|||
check-template-location: false |
|||
profiles: |
|||
active: dev |
|||
jackson: |
|||
time-zone: GMT+8 |
|||
data: |
|||
redis: |
|||
repositories: |
|||
enabled: false |
|||
main: |
|||
allow-bean-definition-overriding: true |
|||
|
|||
#配置 Jpa |
|||
jpa: |
|||
properties: |
|||
hibernate: |
|||
ddl-auto: none |
|||
#dialect: org.hibernate.dialect.MySQL5InnoDBDialect |
|||
# naming: |
|||
# physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl |
|||
open-in-view: true |
|||
|
|||
|
|||
redis: |
|||
#数据库索引 |
|||
database: ${REDIS_DB:0} |
|||
host: ${REDIS_HOST:192.168.99.207} |
|||
port: ${REDIS_PORT:6379} |
|||
password: ${REDIS_PWD:ftzn83560792} |
|||
#连接超时时间 |
|||
timeout: 5000 |
|||
|
|||
task: |
|||
pool: |
|||
# 核心线程池大小 |
|||
core-pool-size: 10 |
|||
# 最大线程数 |
|||
max-pool-size: 30 |
|||
# 活跃时间 |
|||
keep-alive-seconds: 60 |
|||
# 队列容量 |
|||
queue-capacity: 50 |
|||
|
|||
#七牛云 |
|||
qiniu: |
|||
# 文件大小 /M |
|||
max-size: 15 |
|||
|
|||
#邮箱验证码有效时间/秒 |
|||
code: |
|||
expiration: 300 |
|||
|
|||
#密码加密传输,前端公钥加密,后端私钥解密 |
|||
rsa: |
|||
private_key: MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9pB6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZUBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3tTbklZkD2A== |
Write
Preview
Loading…
Cancel
Save
Reference in new issue