feat: init

This commit is contained in:
2025-03-21 18:21:27 +08:00
commit 12c3e69732
454 changed files with 31475 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
### IDEA ###
.DS_Store
~/*
.idea/*
*.iml
*/target/*
*/*.iml
/.gradle/
/application.pid
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "{}" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright 2022-2025 Odboy
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+48
View File
@@ -0,0 +1,48 @@
<h1 style="text-align: center">ELADMIN 后台管理系统</h1>
#### 项目简介
一个基于 Spring Boot 2.7.18 、 Mybatis-Plus、 JWT、Spring Security、Redis、Vue的前后端分离的后台管理系统
**账号密码:** `admin / 123456`
#### 项目源码
https://github.com/odboy-tianjun/cutejava
#### 主要特性
- 使用最新技术栈,社区资源丰富。
- 高效率开发,代码生成器可一键生成前后端代码
- 支持数据字典,可方便地对一些状态进行管理
- 支持接口限流,避免恶意请求导致服务层压力过大
- 支持接口级别的功能权限与数据权限,可自定义操作
- 自定义权限注解与匿名接口注解,可快速对接口拦截与放行
- 对一些常用地前端组件封装:表格数据请求、数据字典等
- 前后端统一异常拦截处理,统一输出异常,避免繁琐的判断
- 支持在线用户管理与服务器性能监控,支持限制单用户登录
#### 系统功能
- 用户管理:提供用户的相关配置,新增用户后,默认密码为123456
- 角色管理:对权限与菜单进行分配,可根据部门设置角色的数据权限
- 菜单管理:已实现菜单动态路由,后端可配置化,支持多级菜单
- 部门管理:可配置系统组织架构,树形表格展示
- 岗位管理:配置各个部门的职位
- 字典管理:可维护常用一些固定的数据,如:状态,性别等
- 系统日志:记录用户操作日志与异常日志,方便开发人员定位排错
- SQL监控:采用druid 监控数据库访问性能,默认用户名admin,密码123456
- 定时任务:整合Quartz做定时任务,加入任务日志,任务运行情况一目了然
- 邮件工具:配合富文本,发送html格式的邮件
- 七牛云存储:可同步七牛云存储的数据到系统,无需登录七牛云直接操作云数据
- 服务监控:监控服务器的负载情况
#### 特别鸣谢
- 感谢 [PanJiaChen](https://github.com/PanJiaChen/vue-element-admin) 大佬提供的前端模板
- 感谢 [Moxun](https://github.com/moxun1639) 大佬提供的前端 Curd 通用组件
- 感谢 [zhy6599](https://gitee.com/zhy6599) 大佬提供的后端运维管理相关功能
- 感谢 [j.yao.SUSE](https://github.com/everhopingandwaiting) 大佬提供的匿名接口与Redis限流等功能
+25
View File
@@ -0,0 +1,25 @@
<?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>cutejava</artifactId>
<groupId>cn.odboy</groupId>
<version>1.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<properties>
<hutool.version>5.8.36</hutool.version>
</properties>
<artifactId>cutejava-common</artifactId>
<name>公共模块</name>
<dependencies>
<!--工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,14 @@
package cn.odboy.annotation.rest;
import java.lang.annotation.*;
/**
* 用于标记匿名访问方法
*/
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}
@@ -0,0 +1,69 @@
package cn.odboy.annotation.rest;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code DELETE} requests onto specific handler
* methods.
* 支持匿名访问 DeleteMapping
*
* @see AnonymousGetMapping
* @see AnonymousPostMapping
* @see AnonymousPutMapping
* @see AnonymousPatchMapping
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.DELETE)
public @interface AnonymousDeleteMapping {
/**
* 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}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
@@ -0,0 +1,68 @@
package cn.odboy.annotation.rest;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code GET} requests onto specific handler
* methods.
* <p>
* 支持匿名访问 GetMapping
*
* @see RequestMapping
*/
@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,69 @@
package cn.odboy.annotation.rest;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code PATCH} requests onto specific handler
* methods.
* * 支持匿名访问 PatchMapping
*
* @see AnonymousGetMapping
* @see AnonymousPostMapping
* @see AnonymousPutMapping
* @see AnonymousDeleteMapping
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.PATCH)
public @interface AnonymousPatchMapping {
/**
* 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}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
@@ -0,0 +1,69 @@
package cn.odboy.annotation.rest;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code POST} requests onto specific handler
* methods.
* 支持匿名访问 PostMapping
*
* @see AnonymousGetMapping
* @see AnonymousPostMapping
* @see AnonymousPutMapping
* @see AnonymousDeleteMapping
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.POST)
public @interface AnonymousPostMapping {
/**
* 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}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
@@ -0,0 +1,69 @@
package cn.odboy.annotation.rest;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code PUT} requests onto specific handler
* methods.
* * 支持匿名访问 PutMapping
*
* @see AnonymousGetMapping
* @see AnonymousPostMapping
* @see AnonymousPutMapping
* @see AnonymousDeleteMapping
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.PUT)
public @interface AnonymousPutMapping {
/**
* 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}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
@@ -0,0 +1,64 @@
package cn.odboy.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.sql.Timestamp;
@Getter
@Setter
public class BaseEntity implements Serializable {
@CreatedBy
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建人", hidden = true)
private String createBy;
@LastModifiedBy
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新人", hidden = true)
private String updateBy;
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间: yyyy-MM-dd HH:mm:ss", hidden = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private Timestamp createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "更新时间: yyyy-MM-dd HH:mm:ss", hidden = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private Timestamp updateTime;
/* 分组校验 */
public @interface Create {
}
/* 分组校验 */
public @interface Update {
}
@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,80 @@
package cn.odboy.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 创建自定义的线程池
*/
@EnableAsync
@Configuration
public class AsyncExecutor implements AsyncConfigurer {
public static int corePoolSize;
public static int maxPoolSize;
public static int keepAliveSeconds;
public static int queueCapacity;
@Value("${task.pool.core-pool-size}")
public void setCorePoolSize(int corePoolSize) {
AsyncExecutor.corePoolSize = corePoolSize;
}
@Value("${task.pool.max-pool-size}")
public void setMaxPoolSize(int maxPoolSize) {
AsyncExecutor.maxPoolSize = maxPoolSize;
}
@Value("${task.pool.keep-alive-seconds}")
public void setKeepAliveSeconds(int keepAliveSeconds) {
AsyncExecutor.keepAliveSeconds = keepAliveSeconds;
}
@Value("${task.pool.queue-capacity}")
public void setQueueCapacity(int queueCapacity) {
AsyncExecutor.queueCapacity = queueCapacity;
}
/**
* 自定义线程池,用法 @Async
*
* @return Executor
*/
@Override
public Executor getAsyncExecutor() {
// 自定义工厂
ThreadFactory factory = r -> new Thread(r, "el-async-" + new AtomicInteger(1).getAndIncrement());
// 自定义线程池
return new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveSeconds,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueCapacity), factory,
new ThreadPoolExecutor.CallerRunsPolicy());
}
/**
* 自定义线程池,用法,注入到类中使用
* private ThreadPoolTaskExecutor taskExecutor;
*
* @return ThreadPoolTaskExecutor
*/
@Bean("taskAsync")
public ThreadPoolTaskExecutor taskAsync() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(20);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("el-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
@@ -0,0 +1,26 @@
package cn.odboy.config;
import cn.odboy.util.SecurityUtil;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Service(value = "el")
public class AuthorityConfig {
/**
* 判断接口是否有权限
*
* @param permissions 权限
* @return /
*/
public Boolean check(String... permissions) {
// 获取当前用户的所有权限
List<String> elPermissions = SecurityUtil.getCurrentUser().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 判断当前用户的所有权限是否包含接口上定义的权限
return elPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(elPermissions::contains);
}
}
@@ -0,0 +1,178 @@
package cn.odboy.config;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONFactory;
import com.alibaba.fastjson2.JSONWriter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.MurmurHash3;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleCacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfiguration extends CachingConfigurerSupport {
/**
* 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全)
*/
private static final String[] WHITELIST_STR = {"cn.odboy"};
/**
* 设置 redis 数据默认过期时间,默认2小时
* 设置@cacheable 序列化方式
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext.
SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(2));
return configuration;
}
@Bean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 指定 key 和 value 的序列化方案
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// 设置fastJson的序列化白名单
for (String pack : WHITELIST_STR) {
JSONFactory.getDefaultObjectReaderProvider().addAutoTypeAccept(pack);
}
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
/**
* 缓存管理器
*
* @param redisConnectionFactory /
* @return 缓存管理器
*/
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration config = redisCacheConfiguration();
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.build();
}
/**
* 自定义缓存key生成策略
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
Map<String, Object> container = new HashMap<>(8);
Class<?> targetClassClass = target.getClass();
// 类地址
container.put("class", targetClassClass.toGenericString());
// 方法名称
container.put("methodName", method.getName());
// 包名称
container.put("package", targetClassClass.getPackage());
// 参数列表
for (int i = 0; i < params.length; i++) {
container.put(String.valueOf(i), params[i]);
}
// 转为JSON字符串
String jsonString = JSON.toJSONString(container);
// 使用 MurmurHash 生成 hash
return Integer.toHexString(MurmurHash3.hash32x86(jsonString.getBytes()));
};
}
@Bean
@SuppressWarnings({"unchecked", "all"})
public CacheErrorHandler errorHandler() {
return new SimpleCacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
// 处理缓存读取错误
log.error("Cache Get Error: {}", exception.getMessage());
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
// 处理缓存写入错误
log.error("Cache Put Error: {}", exception.getMessage());
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
// 处理缓存删除错误
log.error("Cache Evict Error: {}", exception.getMessage());
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
// 处理缓存清除错误
log.error("Cache Clear Error: {}", exception.getMessage());
}
};
}
/**
* Value 序列化
*
* @param <T>
*/
static class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
private final Class<T> clazz;
FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(StandardCharsets.UTF_8);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length == 0) {
return null;
}
String str = new String(bytes, StandardCharsets.UTF_8);
return JSON.parseObject(str, clazz);
}
}
}
@@ -0,0 +1,56 @@
package cn.odboy.config;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedissonConfiguration {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.database}")
private int redisDatabase;
@Value("${spring.redis.password:}")
private String redisPassword;
@Value("${spring.redis.timeout:5000}")
private int timeout;
@Value("${spring.redis.lettuce.pool.max-active:64}")
private int connectionPoolSize;
@Value("${spring.redis.lettuce.pool.min-idle:16}")
private int connectionMinimumIdleSize;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort)
.setDatabase(redisDatabase)
.setTimeout(timeout)
.setConnectionPoolSize(connectionPoolSize)
.setConnectionMinimumIdleSize(connectionMinimumIdleSize);
if (StrUtil.isNotBlank(redisPassword)) {
config.useSingleServer().setPassword(redisPassword);
}
return Redisson.create(config);
}
}
@@ -0,0 +1,77 @@
package cn.odboy.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
@SuppressWarnings({"unchecked", "all"})
@ConditionalOnWebApplication
@AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled",
havingValue = "true", matchIfMissing = true)
public class RemoveDruidAdConfig {
/**
* 方法名: removeDruidAdFilterRegistrationBean
* 方法描述 除去页面底部的广告
*
* @param properties com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties
* @return org.springframework.boot.web.servlet.FilterRegistrationBean
*/
@Bean
public FilterRegistrationBean removeDruidAdFilterRegistrationBean(DruidStatProperties properties) {
// 获取web监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取common.js的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
final String filePath = "support/http/resources/js/common.js";
//创建filter进行过滤
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (httpRequest.getRequestURI().endsWith("js/common.js")) {
// 获取common.js
String text = Utils.readFromResource(filePath);
// 正则替换banner, 除去底部的广告信息
text = text.replaceAll("<a.*?druid_banner\"></a><br/>", "");
text = text.replaceAll("powered by.*?shrek.wang</a>", "");
httpResponse.setContentType("application/javascript");
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.getWriter().write(text);
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
};
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}
@@ -0,0 +1,57 @@
package cn.odboy.config.mybatis;
import cn.hutool.core.util.StrUtil;
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义 p6spy sql输出格式
*/
@Slf4j
public class CustomP6SpyLogger implements MessageFormattingStrategy {
/**
* 重置颜色
*/
private static final String RESET = "\u001B[0m";
/**
* 红色
*/
private static final String RED = "\u001B[31m";
/**
* 绿色
*/
private static final String GREEN = "\u001B[32m";
/**
* 蓝色
*/
private static final String BLUE = "\u001B[34m";
/**
* 格式化 sql
*
* @param connectionId 连接id
* @param now 当前时间
* @param elapsed 执行时长
* @param category sql分类
* @param prepared 预编译sql
* @param sql 执行sql
* @param url 数据库连接url
* @return 格式化后的sql
*/
@Override
public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) {
// 去掉换行和多余空格
if (StrUtil.isNotBlank(sql)) {
sql = sql.replaceAll("\\s+", " ").trim();
}
// 格式化并加上颜色
return String.format(
"CuteJava - %s%s%s %s[%s] %sREVIEW_SQL [Interval: %dms]%s -> %s;",
RED, now, RESET,
GREEN, Thread.currentThread().getName(),
BLUE,
elapsed, RESET,
sql
);
}
}
@@ -0,0 +1,42 @@
package cn.odboy.config.mybatis;
import cn.hutool.core.date.DateTime;
import cn.odboy.util.SecurityUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.sql.Timestamp;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
/* 创建时间 */
this.strictInsertFill(metaObject, "createTime", Timestamp.class, DateTime.now().toTimestamp());
this.strictInsertFill(metaObject, "updateTime", Timestamp.class, DateTime.now().toTimestamp());
/* 操作人 */
String username = "System";
try {
username = SecurityUtil.getCurrentUsername();
} catch (Exception ignored) {
}
this.strictInsertFill(metaObject, "createBy", String.class, username);
this.strictInsertFill(metaObject, "updateBy", String.class, username);
}
@Override
public void updateFill(MetaObject metaObject) {
/* 更新时间 */
this.strictUpdateFill(metaObject, "updateTime", Timestamp.class, DateTime.now().toTimestamp());
/* 操作人 */
String username = "System";
try {
username = SecurityUtil.getCurrentUsername();
} catch (Exception ignored) {
}
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
}
}
@@ -0,0 +1,25 @@
package cn.odboy.config.mybatis;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/**
* MyBatisPlus拦截器(用于分页)
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加MySQL的分页拦截器
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setOptimizeJoin(true);
paginationInnerInterceptor.setMaxLimit(500L);
paginationInnerInterceptor.setOverflow(false);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
@@ -0,0 +1,46 @@
package cn.odboy.config.properties;
import cn.odboy.constant.SystemConstant;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "file")
public class FileProperties {
/**
* 文件大小限制
*/
private Long maxSize;
/**
* 头像大小限制
*/
private Long avatarMaxSize;
private ElPath mac;
private ElPath linux;
private ElPath windows;
public ElPath getPath() {
String os = System.getProperty("os.name");
if (os.toLowerCase().startsWith(SystemConstant.WIN)) {
return windows;
} else if (os.toLowerCase().startsWith(SystemConstant.MAC)) {
return mac;
}
return linux;
}
@Data
public static class ElPath {
private String path;
private String avatar;
}
}
@@ -0,0 +1,17 @@
package cn.odboy.config.properties;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Data
@Component
public class RsaProperties {
public static String privateKey;
@Value("${rsa.private_key}")
public void setPrivateKey(String privateKey) {
RsaProperties.privateKey = privateKey;
}
}
@@ -0,0 +1,79 @@
package cn.odboy.config.webConfig;
import cn.odboy.config.properties.FileProperties;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.support.config.FastJsonConfig;
import com.alibaba.fastjson2.support.spring.http.converter.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableWebMvc
public class ConfigurerAdapter implements WebMvcConfigurer {
/**
* 文件配置
*/
private final FileProperties properties;
public ConfigurerAdapter(FileProperties properties) {
this.properties = properties;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
FileProperties.ElPath path = properties.getPath();
String avatarUtl = "file:" + path.getAvatar().replace("\\", "/");
String pathUtl = "file:" + path.getPath().replace("\\", "/");
registry.addResourceHandler("/avatar/**").addResourceLocations(avatarUtl).setCachePeriod(0);
registry.addResourceHandler("/file/**").addResourceLocations(pathUtl).setCachePeriod(0);
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/").setCachePeriod(0);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 添加默认的 StringHttpMessageConverter
converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
// 配置 FastJsonHttpMessageConverter
FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
List<MediaType> supportMediaTypeList = new ArrayList<>();
supportMediaTypeList.add(MediaType.APPLICATION_JSON);
FastJsonConfig config = new FastJsonConfig();
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
// 开启引用检测,枚举支持
config.setWriterFeatures(
JSONWriter.Feature.WriteEnumUsingToString,
JSONWriter.Feature.ReferenceDetection
);
fastJsonConverter.setFastJsonConfig(config);
fastJsonConverter.setSupportedMediaTypes(supportMediaTypeList);
fastJsonConverter.setDefaultCharset(StandardCharsets.UTF_8);
// 将 FastJsonHttpMessageConverter 添加到列表末尾
converters.add(fastJsonConverter);
}
}
@@ -0,0 +1,30 @@
package cn.odboy.config.webConfig;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.MultipartConfigElement;
import java.io.File;
@Configuration
public class MultipartConfig {
/**
* 文件上传临时路径
*/
@Bean
MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
String location = System.getProperty("user.home") + "/.eladmin/file/tmp";
File tmpFile = new File(location);
if (!tmpFile.exists()) {
if (!tmpFile.mkdirs()) {
System.out.println("create was not successful.");
}
}
factory.setLocation(location);
return factory.createMultipartConfig();
}
}
@@ -0,0 +1,17 @@
package cn.odboy.config.webConfig;
import org.apache.catalina.connector.Connector;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.annotation.Configuration;
/**
* 允许在查询字符串中使用特定的特殊字符
*/
@Configuration(proxyBeanMethods = false)
public class QueryCustomizer implements TomcatConnectorCustomizer {
@Override
public void customize(Connector connector) {
connector.setProperty("relaxedQueryChars", "[]{}");
}
}
@@ -0,0 +1,148 @@
package cn.odboy.config.webConfig;
import cn.odboy.util.AnonTagUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* api页面 /doc.html
*/
@Configuration
@EnableSwagger2
@RequiredArgsConstructor
public class SwaggerConfig {
@Value("${server.servlet.context-path:}")
private String apiPath;
@Value("${jwt.header}")
private String tokenHeader;
@Value("${swagger.enabled}")
private Boolean enabled;
private final ApplicationContext applicationContext;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.enable(enabled)
.pathMapping("/")
.apiInfo(apiInfo())
.select()
.paths(PathSelectors.regex("^(?!/error).*"))
.paths(PathSelectors.any())
.build()
//添加登陆认证
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.description("一个简单且易上手的 Spring boot 后台管理框架")
.title("ELADMIN 接口文档")
.version("1.6")
.build();
}
private List<SecurityScheme> securitySchemes() {
//设置请求头信息
List<SecurityScheme> securitySchemes = new ArrayList<>();
ApiKey apiKey = new ApiKey(tokenHeader, tokenHeader, "header");
securitySchemes.add(apiKey);
return securitySchemes;
}
private List<SecurityContext> securityContexts() {
//设置需要登录认证的路径
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(getContextByPath());
return securityContexts;
}
private SecurityContext getContextByPath() {
Set<String> urls = AnonTagUtil.getAllAnonymousUrl(applicationContext);
urls = urls.stream().filter(url -> !url.equals("/")).collect(Collectors.toSet());
String regExp = "^(?!" + apiPath + String.join("|" + apiPath, urls) + ").*$";
return SecurityContext.builder()
.securityReferences(defaultAuth())
.operationSelector(o -> o.requestMappingPattern()
// 排除不需要认证的接口
.matches(regExp))
.build();
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> securityReferences = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
securityReferences.add(new SecurityReference(tokenHeader, authorizationScopes));
return securityReferences;
}
/**
* 解决Springfox与SpringBoot集成后,WebMvcRequestHandlerProvider和WebFluxRequestHandlerProvider冲突问题
*
* @return /
*/
@Bean
@SuppressWarnings({"unchecked", "all"})
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return bean;
}
private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
List<T> filteredMappings = mappings.stream()
.filter(mapping -> mapping.getPatternParser() == null)
.collect(Collectors.toList());
mappings.clear();
mappings.addAll(filteredMappings);
}
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
if (field != null) {
field.setAccessible(true);
try {
return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Failed to access handlerMappings field", e);
}
}
return Collections.emptyList();
}
};
}
}
@@ -0,0 +1,15 @@
package cn.odboy.config.webConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@@ -0,0 +1,49 @@
package cn.odboy.constant;
/**
* 关于缓存的Key集合
*/
public interface CacheKey {
/**
* 用户
*/
String USER_ID = "user::id:";
/**
* 数据
*/
String DATA_USER = "data::user:";
/**
* 菜单
*/
String MENU_ID = "menu::id:";
String MENU_USER = "menu::user:";
/**
* 角色授权
*/
String ROLE_AUTH = "role::auth:";
String ROLE_USER = "role::user:";
/**
* 角色信息
*/
String ROLE_ID = "role::id:";
/**
* 部门
*/
String DEPT_ID = "dept::id:";
/**
* 岗位
*/
String JOB_ID = "job::id:";
/**
* 数据字典
*/
String DICT_NAME = "dict::name:";
}
@@ -0,0 +1,31 @@
package cn.odboy.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 验证码业务场景
*/
@Getter
@AllArgsConstructor
public enum CodeBiEnum {
/* 旧邮箱修改邮箱 */
ONE(1, "旧邮箱修改邮箱"),
/* 通过邮箱修改密码 */
TWO(2, "通过邮箱修改密码");
private final Integer code;
private final String description;
public static CodeBiEnum find(Integer code) {
for (CodeBiEnum value : CodeBiEnum.values()) {
if (value.getCode().equals(code)) {
return value;
}
}
return null;
}
}
@@ -0,0 +1,27 @@
package cn.odboy.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 验证码业务场景对应的 Redis 中的 key
*/
@Getter
@AllArgsConstructor
public enum CodeEnum {
/* 通过手机号码重置邮箱 */
PHONE_RESET_EMAIL_CODE("phone_reset_email_code_", "通过手机号码重置邮箱"),
/* 通过旧邮箱重置邮箱 */
EMAIL_RESET_EMAIL_CODE("email_reset_email_code_", "通过旧邮箱重置邮箱"),
/* 通过手机号码重置密码 */
PHONE_RESET_PWD_CODE("phone_reset_pwd_code_", "通过手机号码重置密码"),
/* 通过邮箱重置密码 */
EMAIL_RESET_PWD_CODE("email_reset_pwd_code_", "通过邮箱重置密码");
private final String key;
private final String description;
}
@@ -0,0 +1,34 @@
package cn.odboy.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 数据权限枚举
*/
@Getter
@AllArgsConstructor
public enum DataScopeEnum {
/* 全部的数据权限 */
ALL("全部", "全部的数据权限"),
/* 自己部门的数据权限 */
THIS_LEVEL("本级", "自己部门的数据权限"),
/* 自定义的数据权限 */
CUSTOMIZE("自定义", "自定义的数据权限");
private final String value;
private final String description;
public static DataScopeEnum find(String val) {
for (DataScopeEnum dataScopeEnum : DataScopeEnum.values()) {
if (dataScopeEnum.getValue().equals(val)) {
return dataScopeEnum;
}
}
return null;
}
}
@@ -0,0 +1,53 @@
package cn.odboy.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum RequestMethodEnum {
/**
* 搜寻 @AnonymousGetMapping
*/
GET("GET"),
/**
* 搜寻 @AnonymousPostMapping
*/
POST("POST"),
/**
* 搜寻 @AnonymousPutMapping
*/
PUT("PUT"),
/**
* 搜寻 @AnonymousPatchMapping
*/
PATCH("PATCH"),
/**
* 搜寻 @AnonymousDeleteMapping
*/
DELETE("DELETE"),
/**
* 否则就是所有 Request 接口都放行
*/
ALL("All");
/**
* Request 类型
*/
private final String type;
public static RequestMethodEnum find(String type) {
for (RequestMethodEnum value : RequestMethodEnum.values()) {
if (value.getType().equals(type)) {
return value;
}
}
return ALL;
}
}
@@ -0,0 +1,16 @@
package cn.odboy.constant;
/**
* 常用静态常量
*/
public class SystemConstant {
/**
* win 系统
*/
public static final String WIN = "win";
/**
* mac 系统
*/
public static final String MAC = "mac";
}
@@ -0,0 +1,24 @@
package cn.odboy.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
/**
* 统一异常处理
*/
@Getter
public class BadRequestException extends RuntimeException {
private Integer status = BAD_REQUEST.value();
public BadRequestException(String msg) {
super(msg);
}
public BadRequestException(HttpStatus status, String msg) {
super(msg);
this.status = status.value();
}
}
@@ -0,0 +1,16 @@
package cn.odboy.exception;
import org.springframework.util.StringUtils;
public class EntityExistException extends RuntimeException {
public EntityExistException(Class clazz, String field, String val) {
super(EntityExistException.generateMessage(clazz.getSimpleName(), field, val));
}
private static String generateMessage(String entity, String field, String val) {
return StringUtils.capitalize(entity)
+ " with " + field + " " + val + " existed";
}
}
@@ -0,0 +1,15 @@
package cn.odboy.exception;
import org.springframework.util.StringUtils;
public class EntityNotFoundException extends RuntimeException {
public EntityNotFoundException(Class clazz, String field, String val) {
super(EntityNotFoundException.generateMessage(clazz.getSimpleName(), field, val));
}
private static String generateMessage(String entity, String field, String val) {
return StringUtils.capitalize(entity)
+ " with " + field + " " + val + " does not exist";
}
}
@@ -0,0 +1,31 @@
package cn.odboy.exception.handler;
import lombok.Data;
@Data
public class ApiError {
private Integer status = 400;
private Long timestamp;
private String message;
private ApiError() {
timestamp = System.currentTimeMillis();
}
public static ApiError error(String message) {
ApiError apiError = new ApiError();
apiError.setMessage(message);
return apiError;
}
public static ApiError error(Integer status, String message) {
ApiError apiError = new ApiError();
apiError.setStatus(status);
apiError.setMessage(message);
return apiError;
}
}
@@ -0,0 +1,95 @@
package cn.odboy.exception.handler;
import cn.odboy.exception.BadRequestException;
import cn.odboy.exception.EntityExistException;
import cn.odboy.exception.EntityNotFoundException;
import cn.odboy.util.ThrowableUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import static org.springframework.http.HttpStatus.NOT_FOUND;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理所有不可知的异常
*/
@ExceptionHandler(Throwable.class)
public ResponseEntity<ApiError> handleException(Throwable e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return buildResponseEntity(ApiError.error(e.getMessage()));
}
/**
* BadCredentialsException
*/
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiError> badCredentialsException(BadCredentialsException e) {
// 打印堆栈信息
String message = "坏的凭证".equals(e.getMessage()) ? "用户名或密码不正确" : e.getMessage();
log.error(message);
return buildResponseEntity(ApiError.error(message));
}
/**
* 处理自定义异常
*/
@ExceptionHandler(value = BadRequestException.class)
public ResponseEntity<ApiError> badRequestException(BadRequestException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return buildResponseEntity(ApiError.error(e.getStatus(), e.getMessage()));
}
/**
* 处理 EntityExist
*/
@ExceptionHandler(value = EntityExistException.class)
public ResponseEntity<ApiError> entityExistException(EntityExistException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return buildResponseEntity(ApiError.error(e.getMessage()));
}
/**
* 处理 EntityNotFound
*/
@ExceptionHandler(value = EntityNotFoundException.class)
public ResponseEntity<ApiError> entityNotFoundException(EntityNotFoundException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return buildResponseEntity(ApiError.error(NOT_FOUND.value(), e.getMessage()));
}
/**
* 处理所有接口数据验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
String message = objectError.getDefaultMessage();
if (objectError instanceof FieldError) {
message = ((FieldError) objectError).getField() + ": " + message;
}
return buildResponseEntity(ApiError.error(message));
}
/**
* 统一返回
*/
private ResponseEntity<ApiError> buildResponseEntity(ApiError apiError) {
return new ResponseEntity<>(apiError, HttpStatus.valueOf(apiError.getStatus()));
}
}
@@ -0,0 +1,88 @@
package cn.odboy.util;
import cn.odboy.annotation.rest.AnonymousAccess;
import cn.odboy.constant.RequestMethodEnum;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.*;
/**
* 匿名标记工具
*/
public class AnonTagUtil {
/**
* 获取匿名标记的URL
*
* @param applicationContext /
* @return /
*/
public static Map<String, Set<String>> getAnonymousUrl(ApplicationContext applicationContext) {
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
Map<String, Set<String>> anonymousUrls = new HashMap<>(8);
// 获取匿名标记
Set<String> get = new HashSet<>();
Set<String> post = new HashSet<>();
Set<String> put = new HashSet<>();
Set<String> patch = new HashSet<>();
Set<String> delete = new HashSet<>();
Set<String> all = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
if (null != anonymousAccess) {
List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
RequestMethodEnum request = RequestMethodEnum.find(requestMethods.isEmpty() ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
if (infoEntry.getKey().getPatternsCondition() != null) {
switch (Objects.requireNonNull(request)) {
case GET:
get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case POST:
post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PUT:
put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PATCH:
patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case DELETE:
delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
default:
all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
}
}
}
}
anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
return anonymousUrls;
}
/**
* 获取所有匿名标记的URL
*
* @param applicationContext /
* @return /
*/
public static Set<String> getAllAnonymousUrl(ApplicationContext applicationContext) {
Set<String> allUrl = new HashSet<>();
Map<String, Set<String>> anonymousUrls = getAnonymousUrl(applicationContext);
for (String key : anonymousUrls.keySet()) {
allUrl.addAll(anonymousUrls.get(key));
}
return allUrl;
}
}
@@ -0,0 +1,134 @@
package cn.odboy.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 计算类
*/
public class BigDecimalUtil {
/**
* 将对象转换为 BigDecimal
*
* @param obj 输入对象
* @return 转换后的 BigDecimal
*/
private static BigDecimal toBigDecimal(Object obj) {
if (obj instanceof BigDecimal) {
return (BigDecimal) obj;
} else if (obj instanceof Long) {
return BigDecimal.valueOf((Long) obj);
} else if (obj instanceof Integer) {
return BigDecimal.valueOf((Integer) obj);
} else if (obj instanceof Double) {
return new BigDecimal(String.valueOf(obj));
} else {
throw new IllegalArgumentException("Unsupported type");
}
}
/**
* 加法
*
* @param a 加数
* @param b 加数
* @return 两个加数的和,保留两位小数
*/
public static BigDecimal add(Object a, Object b) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.add(bdB).setScale(2, RoundingMode.HALF_UP);
}
/**
* 减法
*
* @param a 被减数
* @param b 减数
* @return 两数的差,保留两位小数
*/
public static BigDecimal subtract(Object a, Object b) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.subtract(bdB).setScale(2, RoundingMode.HALF_UP);
}
/**
* 乘法
*
* @param a 乘数
* @param b 乘数
* @return 两个乘数的积,保留两位小数
*/
public static BigDecimal multiply(Object a, Object b) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.multiply(bdB).setScale(2, RoundingMode.HALF_UP);
}
/**
* 除法
*
* @param a 被除数
* @param b 除数
* @return 两数的商,保留两位小数
*/
public static BigDecimal divide(Object a, Object b) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.divide(bdB, 2, RoundingMode.HALF_UP);
}
/**
* 除法
*
* @param a 被除数
* @param b 除数
* @param scale 保留小数位数
* @return 两数的商,保留两位小数
*/
public static BigDecimal divide(Object a, Object b, int scale) {
BigDecimal bdA = toBigDecimal(a);
BigDecimal bdB = toBigDecimal(b);
return bdA.divide(bdB, scale, RoundingMode.HALF_UP);
}
/**
* 分转元
*
* @param obj 分的金额
* @return 转换后的元,保留两位小数
*/
public static BigDecimal centsToYuan(Object obj) {
BigDecimal cents = toBigDecimal(obj);
return cents.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
}
/**
* 元转分
*
* @param obj 元的金额
* @return 转换后的分
*/
public static Long yuanToCents(Object obj) {
BigDecimal yuan = toBigDecimal(obj);
return yuan.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).longValue();
}
public static void main(String[] args) {
BigDecimal num1 = new BigDecimal("10.123");
BigDecimal num2 = new BigDecimal("2.456");
System.out.println("加法结果: " + add(num1, num2));
System.out.println("减法结果: " + subtract(num1, num2));
System.out.println("乘法结果: " + multiply(num1, num2));
System.out.println("除法结果: " + divide(num1, num2));
Long cents = 12345L;
System.out.println("分转元结果: " + centsToYuan(cents));
BigDecimal yuan = new BigDecimal("123.45");
System.out.println("元转分结果: " + yuanToCents(yuan));
}
}
@@ -0,0 +1,29 @@
package cn.odboy.util;
import java.io.Closeable;
/**
* 用于关闭各种连接,缺啥补啥
*/
public class CloseUtil {
public static void close(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
// 静默关闭
}
}
}
public static void close(AutoCloseable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
// 静默关闭
}
}
}
}
@@ -0,0 +1,152 @@
package cn.odboy.util;
import java.sql.Timestamp;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;
/**
* JDK 8 新日期类 格式化与字符串转换 工具类
*/
public class DateUtil {
public static final DateTimeFormatter DFY_MD_HMS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final DateTimeFormatter DFY_MD = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* LocalDateTime 转时间戳
*
* @param localDateTime /
* @return /
*/
public static Long getTimeStamp(LocalDateTime localDateTime) {
return localDateTime.atZone(ZoneId.systemDefault()).toEpochSecond();
}
/**
* 时间戳转LocalDateTime
*
* @param timeStamp /
* @return /
*/
public static LocalDateTime fromTimeStamp(Long timeStamp) {
return LocalDateTime.ofEpochSecond(timeStamp, 0, OffsetDateTime.now().getOffset());
}
/**
* LocalDateTime 转 Date
* Jdk8 后 不推荐使用 {@link Date} Date
*
* @param localDateTime /
* @return /
*/
public static Date toDate(LocalDateTime localDateTime) {
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
/**
* LocalDate 转 Date
* Jdk8 后 不推荐使用 {@link Date} Date
*
* @param localDate /
* @return /
*/
public static Date toDate(LocalDate localDate) {
return toDate(localDate.atTime(LocalTime.now(ZoneId.systemDefault())));
}
/**
* Date转 LocalDateTime
* Jdk8 后 不推荐使用 {@link Date} Date
*
* @param date /
* @return /
*/
public static LocalDateTime toLocalDateTime(Date date) {
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}
/**
* 日期 格式化
*
* @param localDateTime /
* @param patten /
* @return /
*/
public static String localDateTimeFormat(LocalDateTime localDateTime, String patten) {
DateTimeFormatter df = DateTimeFormatter.ofPattern(patten);
return df.format(localDateTime);
}
/**
* 日期 格式化
*
* @param localDateTime /
* @param df /
* @return /
*/
public static String localDateTimeFormat(LocalDateTime localDateTime, DateTimeFormatter df) {
return df.format(localDateTime);
}
/**
* 日期格式化 yyyy-MM-dd HH:mm:ss
*
* @param localDateTime /
* @return /
*/
public static String localDateTimeFormatyMdHms(LocalDateTime localDateTime) {
return DFY_MD_HMS.format(localDateTime);
}
/**
* 获取当前时间
*
* @return 、
*/
public static Timestamp getTimeStamp() {
return Timestamp.valueOf(LocalDateTime.now());
}
/**
* 日期格式化 yyyy-MM-dd
*
* @param localDateTime /
* @return /
*/
public String localDateTimeFormatyMd(LocalDateTime localDateTime) {
return DFY_MD.format(localDateTime);
}
/**
* 字符串转 LocalDateTime ,字符串格式 yyyy-MM-dd
*
* @param localDateTime /
* @return /
*/
public static LocalDateTime parseLocalDateTimeFormat(String localDateTime, String pattern) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern);
return LocalDateTime.from(dateTimeFormatter.parse(localDateTime));
}
/**
* 字符串转 LocalDateTime ,字符串格式 yyyy-MM-dd
*
* @param localDateTime /
* @return /
*/
public static LocalDateTime parseLocalDateTimeFormat(String localDateTime, DateTimeFormatter dateTimeFormatter) {
return LocalDateTime.from(dateTimeFormatter.parse(localDateTime));
}
/**
* 字符串转 LocalDateTime ,字符串格式 yyyy-MM-dd HH:mm:ss
*
* @param localDateTime /
* @return /
*/
public static LocalDateTime parseLocalDateTimeFormatyMdHms(String localDateTime) {
return LocalDateTime.from(DFY_MD_HMS.parse(localDateTime));
}
}
@@ -0,0 +1,79 @@
package cn.odboy.util;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.nio.charset.StandardCharsets;
/**
* 加密
*/
public class EncryptUtil {
private static final String STR_PARAM = "Passw0rd";
private static final IvParameterSpec IV = new IvParameterSpec(STR_PARAM.getBytes(StandardCharsets.UTF_8));
private static DESKeySpec getDesKeySpec(String source) throws Exception {
if (source == null || source.isEmpty()) {
return null;
}
String strKey = "Passw0rd";
return new DESKeySpec(strKey.getBytes(StandardCharsets.UTF_8));
}
/**
* 对称加密
*/
public static String desEncrypt(String source) throws Exception {
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
DESKeySpec desKeySpec = getDesKeySpec(source);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IV);
return byte2hex(cipher.doFinal(source.getBytes(StandardCharsets.UTF_8))).toUpperCase();
}
/**
* 对称解密
*/
public static String desDecrypt(String source) throws Exception {
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
byte[] src = hex2byte(source.getBytes(StandardCharsets.UTF_8));
DESKeySpec desKeySpec = getDesKeySpec(source);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
cipher.init(Cipher.DECRYPT_MODE, secretKey, IV);
byte[] retByte = cipher.doFinal(src);
return new String(retByte);
}
private static String byte2hex(byte[] inStr) {
String stmp;
StringBuilder out = new StringBuilder(inStr.length * 2);
for (byte b : inStr) {
stmp = Integer.toHexString(b & 0xFF);
if (stmp.length() == 1) {
out.append("0").append(stmp);
} else {
out.append(stmp);
}
}
return out.toString();
}
private static byte[] hex2byte(byte[] b) {
int size = 2;
if ((b.length % size) != 0) {
throw new IllegalArgumentException("长度不是偶数");
}
byte[] b2 = new byte[b.length / 2];
for (int n = 0; n < b.length; n += size) {
String item = new String(b, n, 2);
b2[n / 2] = (byte) Integer.parseInt(item, 16);
}
return b2;
}
}
@@ -0,0 +1,396 @@
package cn.odboy.util;
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 cn.odboy.exception.BadRequestException;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.streaming.SXSSFSheet;
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.nio.file.Files;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* File工具类,扩展 hutool 工具包
*/
@Slf4j
public class FileUtil extends cn.hutool.core.io.FileUtil {
/**
* 系统临时目录
* <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 = new File(SYS_TEM_DIR + 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.isEmpty())) {
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.isEmpty())) {
int dot = filename.lastIndexOf('.');
if (dot > -1) {
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) {
File file = new File(SYS_TEM_DIR + name);
if (file.exists()) {
return file;
}
OutputStream os = null;
try {
os = Files.newOutputStream(file.toPath());
int bytesRead;
int len = 8192;
byte[] buffer = new byte[len];
while ((bytesRead = ins.read(buffer, 0, len)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
CloseUtil.close(os);
CloseUtil.close(ins);
}
return file;
}
/**
* 将文件名解析成文件的上传路径
*/
public static File upload(MultipartFile file, String filePath) {
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS");
// 过滤非法文件名
String name = getFileNameNoEx(verifyFilename(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);
// 处理数据以防止CSV注入
List<Map<String, Object>> sanitizedList = list.parallelStream().map(map -> {
Map<String, Object> sanitizedMap = new LinkedHashMap<>();
map.forEach((key, value) -> {
if (value instanceof String) {
String strValue = (String) value;
// 检查并处理以特殊字符开头的值
if (strValue.startsWith("=") || strValue.startsWith("+") || strValue.startsWith("-") || strValue.startsWith("@")) {
strValue = "'" + strValue; // 添加单引号前缀
}
sanitizedMap.put(key, strValue);
} else {
sanitizedMap.put(key, value);
}
});
return sanitizedMap;
}).collect(Collectors.toList());
// 一次性写出内容,使用默认样式,强制输出标题
writer.write(sanitizedList, true);
SXSSFSheet sheet = (SXSSFSheet) writer.getSheet();
//上面需要强转SXSSFSheet 不然没有trackAllColumnsForAutoSizing方法
sheet.trackAllColumnsForAutoSizing();
//列宽自适应
writer.autoSizeColumnAll();
//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);
}
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 BadRequestException("文件超出规定大小:" + maxSize + "MB");
}
}
/**
* 判断两个文件是否相同
*/
public static boolean check(File file1, File file2) {
String img1Md5 = getMd5(file1);
String img2Md5 = getMd5(file2);
if (img1Md5 != null) {
return img1Md5.equals(img2Md5);
}
return false;
}
/**
* 判断两个文件是否相同
*/
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()];
InputStream in = null;
try {
in = Files.newInputStream(file.toPath());
try {
System.out.println(in.read(b));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
} finally {
CloseUtil.close(in);
}
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);
}
}
}
}
/**
* 验证并过滤非法的文件名
*
* @param fileName 文件名
* @return 文件名
*/
public static String verifyFilename(String fileName) {
// 过滤掉特殊字符
fileName = fileName.replaceAll("[\\\\/:*?\"<>|~\\s]", "");
// 去掉文件名开头和结尾的空格和点
fileName = fileName.trim().replaceAll("^[. ]+|[. ]+$", "");
// 不允许文件名超过255(在Mac和Linux中)或260(在Windows中)个字符
int maxFileNameLength = 255;
if (System.getProperty("os.name").startsWith("Windows")) {
maxFileNameLength = 260;
}
if (fileName.length() > maxFileNameLength) {
fileName = fileName.substring(0, maxFileNameLength);
}
// 过滤掉控制字符
fileName = fileName.replaceAll("[\\p{Cntrl}]", "");
// 过滤掉 ".." 路径
fileName = fileName.replaceAll("\\.{2,}", "");
// 去掉文件名开头的 ".."
fileName = fileName.replaceAll("^\\.+/", "");
// 保留文件名中最后一个 "." 字符,过滤掉其他 "."
fileName = fileName.replaceAll("^(.*)(\\.[^.]*)$", "$1").replaceAll("\\.", "") +
fileName.replaceAll("^(.*)(\\.[^.]*)$", "$2");
return fileName;
}
public static String getMd5(File file) {
return getMd5(getByte(file));
}
}
@@ -0,0 +1,21 @@
package cn.odboy.util;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 分页结果封装类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> implements Serializable {
private List<T> content;
private long totalElements;
}
@@ -0,0 +1,55 @@
package cn.odboy.util;
import com.baomidou.mybatisplus.core.metadata.IPage;
import java.util.Collections;
import java.util.List;
/**
* 分页工具
*/
public class PageUtil extends cn.hutool.core.util.PageUtil {
/**
* List 分页
*/
public static <T> List<T> paging(int page, int size, List<T> list) {
int fromIndex = page * size;
int toIndex = page * size + size;
if (fromIndex > list.size()) {
return Collections.emptyList();
} else if (toIndex >= list.size()) {
return list.subList(fromIndex, list.size());
} else {
return list.subList(fromIndex, toIndex);
}
}
/**
* Page 数据处理
*/
public static <T> PageResult<T> toPage(IPage<T> page) {
return new PageResult<>(page.getRecords(), page.getTotal());
}
/**
* 自定义分页
*/
public static <T> PageResult<T> toPage(List<T> list) {
return new PageResult<>(list, list.size());
}
/**
* 返回空数据
*/
public static <T> PageResult<T> noData() {
return new PageResult<>(null, 0);
}
/**
* 自定义分页
*/
public static <T> PageResult<T> toPage(List<T> list, long totalElements) {
return new PageResult<>(list, totalElements);
}
}
@@ -0,0 +1,790 @@
package cn.odboy.util;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
@SuppressWarnings({"unchecked", "all"})
public class RedisUtil {
private static final Logger log = LoggerFactory.getLogger(RedisUtil.class);
private RedisTemplate<Object, Object> redisTemplate;
public RedisUtil(RedisTemplate<Object, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.redisTemplate.setKeySerializer(new StringRedisSerializer());
this.redisTemplate.setHashKeySerializer(new StringRedisSerializer());
}
/**
* 指定缓存失效时间
*
* @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 key 可以传一个值 或多个
*/
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) {
if (redisTemplate.hasKey(key))
keySet.add(key);
}
long count = redisTemplate.delete(keySet);
log.debug("--------------------------------------------");
log.debug("成功删除缓存:" + keySet.toString());
log.debug("缓存删除数量:" + count + "个");
log.debug("--------------------------------------------");
}
}
}
/**
* 批量模糊删除key
*
* @param pattern
*/
public void scanDel(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
try (Cursor<byte[]> cursor = redisTemplate.executeWithStickyConnection(
(RedisCallback<Cursor<byte[]>>) connection -> (Cursor<byte[]>) new ConvertingCursor<>(
connection.scan(options), redisTemplate.getKeySerializer()::deserialize))) {
while (cursor.hasNext()) {
redisTemplate.delete(cursor.next());
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public <T> T get(String key, Class<T> clazz) {
Object value = key == null ? null : redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
}
if (clazz.isInstance(value)) {
return clazz.cast(value);
} else {
return null;
}
}
/**
* 普通缓存获取
*
* @param key 键
* @param clazz 列表中元素的类型
* @return 值
*/
public <T> List<T> getList(String key, Class<T> clazz) {
Object value = key == null ? null : redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
}
if (value instanceof List<?>) {
List<?> list = (List<?>) value;
// 检查每个元素是否为指定类型
if (list.stream().allMatch(clazz::isInstance)) {
return list.stream().map(clazz::cast).collect(Collectors.toList());
}
}
return null;
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public String getStr(String key) {
if (StrUtil.isBlank(key)) {
return null;
}
Object value = redisTemplate.opsForValue().get(key);
if (value == null) {
return null;
} else {
return String.valueOf(value);
}
}
/**
* 批量获取
*
* @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) {
int attempt = 0;
while (attempt < 3) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
attempt++;
log.error("Attempt {} failed: {}", attempt, 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 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @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);
}
// ============================incr=============================
/**
* 递增
*
* @param key
* @return
*/
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key);
}
/**
* 递减
*
* @param key
* @return
*/
public Long decrement(String key) {
return redisTemplate.opsForValue().decrement(key);
}
}
@@ -0,0 +1,17 @@
package cn.odboy.util;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* 获取 HttpServletRequest
*/
public class RequestHolder {
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
}
@@ -0,0 +1,198 @@
package cn.odboy.util;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* Rsa 工具类,公钥私钥生成,加解密
*/
public class RsaUtil {
private static final String SRC = "123456";
public static void main(String[] args) throws Exception {
System.out.println("\n");
RsaKeyPair keyPair = generateKeyPair();
System.out.println("公钥:" + keyPair.getPublicKey());
System.out.println("私钥:" + keyPair.getPrivateKey());
System.out.println("\n");
test1(keyPair);
System.out.println("\n");
test2(keyPair);
System.out.println("\n");
}
/**
* 公钥加密私钥解密
*/
private static void test1(RsaKeyPair keyPair) throws Exception {
System.out.println("***************** 公钥加密私钥解密开始 *****************");
String text1 = encryptByPublicKey(keyPair.getPublicKey(), RsaUtil.SRC);
String text2 = decryptByPrivateKey(keyPair.getPrivateKey(), text1);
System.out.println("加密前:" + RsaUtil.SRC);
System.out.println("加密后:" + text1);
System.out.println("解密后:" + text2);
if (RsaUtil.SRC.equals(text2)) {
System.out.println("解密字符串和原始字符串一致,解密成功");
} else {
System.out.println("解密字符串和原始字符串不一致,解密失败");
}
System.out.println("***************** 公钥加密私钥解密结束 *****************");
}
/**
* 私钥加密公钥解密
*
* @throws Exception /
*/
private static void test2(RsaKeyPair keyPair) throws Exception {
System.out.println("***************** 私钥加密公钥解密开始 *****************");
String text1 = encryptByPrivateKey(keyPair.getPrivateKey(), RsaUtil.SRC);
String text2 = decryptByPublicKey(keyPair.getPublicKey(), text1);
System.out.println("加密前:" + RsaUtil.SRC);
System.out.println("加密后:" + text1);
System.out.println("解密后:" + text2);
if (RsaUtil.SRC.equals(text2)) {
System.out.println("解密字符串和原始字符串一致,解密成功");
} else {
System.out.println("解密字符串和原始字符串不一致,解密失败");
}
System.out.println("***************** 私钥加密公钥解密结束 *****************");
}
/**
* 公钥解密
*
* @param publicKeyText 公钥
* @param text 待解密的信息
* @return /
* @throws Exception /
*/
public static String decryptByPublicKey(String publicKeyText, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] result = doLongerCipherFinal(Cipher.DECRYPT_MODE, cipher, Base64.decodeBase64(text));
return new String(result);
}
/**
* 私钥加密
*
* @param privateKeyText 私钥
* @param text 待加密的信息
* @return /
* @throws Exception /
*/
public static String encryptByPrivateKey(String privateKeyText, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] result = doLongerCipherFinal(Cipher.ENCRYPT_MODE, cipher, text.getBytes());
return Base64.encodeBase64String(result);
}
/**
* 私钥解密
*
* @param privateKeyText 私钥
* @param text 待解密的文本
* @return /
* @throws Exception /
*/
public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = doLongerCipherFinal(Cipher.DECRYPT_MODE, cipher, Base64.decodeBase64(text));
return new String(result);
}
/**
* 公钥加密
*
* @param publicKeyText 公钥
* @param text 待加密的文本
* @return /
*/
public static String encryptByPublicKey(String publicKeyText, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = doLongerCipherFinal(Cipher.ENCRYPT_MODE, cipher, text.getBytes());
return Base64.encodeBase64String(result);
}
private static byte[] doLongerCipherFinal(int opMode, Cipher cipher, byte[] source) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (opMode == Cipher.DECRYPT_MODE) {
out.write(cipher.doFinal(source));
} else {
int offset = 0;
int totalSize = source.length;
while (totalSize - offset > 0) {
int size = Math.min(cipher.getOutputSize(0) - 11, totalSize - offset);
out.write(cipher.doFinal(source, offset, size));
offset += size;
}
}
out.close();
return out.toByteArray();
}
/**
* 构建RSA密钥对
*
* @return /
* @throws NoSuchAlgorithmException /
*/
public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
return new RsaKeyPair(publicKeyString, privateKeyString);
}
/**
* RSA密钥对对象
*/
public static class RsaKeyPair {
private final String publicKey;
private final String privateKey;
public RsaKeyPair(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
}
}
@@ -0,0 +1,134 @@
package cn.odboy.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.odboy.constant.DataScopeEnum;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Objects;
/**
* 获取当前登录的用户
*/
@Slf4j
@Component
public class SecurityUtil {
public static String header;
public static String tokenStartWith;
@Value("${jwt.header}")
public void setHeader(String header) {
SecurityUtil.header = header;
}
@Value("${jwt.token-start-with}")
public void setTokenStartWith(String tokenStartWith) {
SecurityUtil.tokenStartWith = tokenStartWith;
}
/**
* 获取当前登录的用户
*
* @return UserDetails
*/
public static UserDetails getCurrentUser() {
UserDetailsService userDetailsService = SpringBeanHolder.getBean(UserDetailsService.class);
return userDetailsService.loadUserByUsername(getCurrentUsername());
}
/**
* 获取当前用户的数据权限
*
* @return /
*/
public static List<Long> getCurrentUserDataScope() {
UserDetails userDetails = getCurrentUser();
// 将 Java 对象转换为 JSONObject 对象
JSONObject jsonObject = (JSONObject) JSON.toJSON(userDetails);
JSONArray jsonArray = jsonObject.getJSONArray("dataScopes");
return JSON.parseArray(jsonArray.toJSONString(), Long.class);
}
/**
* 获取数据权限级别
*
* @return 级别
*/
public static String getDataScopeType() {
List<Long> dataScopes = getCurrentUserDataScope();
if (CollUtil.isEmpty(dataScopes)) {
return "";
}
return DataScopeEnum.ALL.getValue();
}
/**
* 获取用户ID
*
* @return 系统用户ID
*/
public static Long getCurrentUserId() {
return getCurrentUserId(getToken());
}
/**
* 获取用户ID
*
* @return 系统用户ID
*/
public static Long getCurrentUserId(String token) {
JWT jwt = JWTUtil.parseToken(token);
return Long.valueOf(jwt.getPayload("userId").toString());
}
/**
* 获取系统用户名称
*
* @return 系统用户名称
*/
public static String getCurrentUsername() {
return getCurrentUsername(getToken());
}
/**
* 获取系统用户名称
*
* @return 系统用户名称
*/
public static String getCurrentUsername(String token) {
JWT jwt = JWTUtil.parseToken(token);
return jwt.getPayload("sub").toString();
}
/**
* 获取Token
*
* @return /
*/
public static String getToken() {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder
.getRequestAttributes())).getRequest();
String bearerToken = request.getHeader(header);
if (bearerToken != null && bearerToken.startsWith(tokenStartWith)) {
// 去掉令牌前缀
return bearerToken.replace(tokenStartWith, "");
} else {
log.debug("非法Token:{}", bearerToken);
}
return null;
}
}
@@ -0,0 +1,158 @@
package cn.odboy.util;
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.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Slf4j
@SuppressWarnings({"unchecked", "all"})
public class SpringBeanHolder implements ApplicationContextAware, DisposableBean {
private static ApplicationContext applicationContext = null;
private static final List<CallBack> CALL_BACKS = new ArrayList<>();
private static boolean addCallback = true;
/**
* 针对 某些初始化方法,在SpringContextHolder 未初始化时 提交回调方法。
* 在SpringContextHolder 初始化后,进行回调使用
*
* @param callBack 回调函数
*/
public synchronized static void addCallBacks(CallBack callBack) {
if (addCallback) {
SpringBeanHolder.CALL_BACKS.add(callBack);
} else {
log.warn("CallBack:{} 已无法添加!立即执行", callBack.getCallBackName());
callBack.executor();
}
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
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() {
SpringBeanHolder.clearHolder();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringBeanHolder.applicationContext != null) {
log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringBeanHolder.applicationContext);
}
SpringBeanHolder.applicationContext = applicationContext;
if (addCallback) {
for (CallBack callBack : SpringBeanHolder.CALL_BACKS) {
callBack.executor();
}
CALL_BACKS.clear();
}
SpringBeanHolder.addCallback = false;
}
/**
* 获取 @Service 的所有 bean 名称
*
* @return /
*/
public static List<String> getAllServiceBeanName() {
return new ArrayList<>(Arrays.asList(applicationContext
.getBeanNamesForAnnotation(Service.class)));
}
interface CallBack {
/**
* 回调执行方法
*/
void executor();
/**
* 本回调任务名称
*
* @return /
*/
default String getCallBackName() {
return Thread.currentThread().getId() + ":" + this.getClass().getName();
}
}
}
@@ -0,0 +1,228 @@
package cn.odboy.util;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import lombok.extern.slf4j.Slf4j;
import net.dreamlu.mica.ip2region.core.Ip2regionSearcher;
import net.dreamlu.mica.ip2region.core.IpInfo;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.*;
/**
* 字符串工具类, 继承org.apache.commons.lang3.StringUtils类
*/
@Slf4j
public class StringUtil extends org.apache.commons.lang3.StringUtils {
private static final char SEPARATOR = '_';
private static final String UNKNOWN = "unknown";
/**
* 注入bean
*/
private final static Ip2regionSearcher IP_SEARCHER = SpringBeanHolder.getBean(Ip2regionSearcher.class);
/**
* 驼峰命名法工具
*
* @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.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || 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) {
IpInfo ipInfo = IP_SEARCHER.memorySearch(ip);
if (ipInfo != null) {
return ipInfo.getAddress();
}
return null;
}
/**
* 获取浏览器
*/
public static String getBrowser(HttpServletRequest request) {
UserAgent ua = UserAgentUtil.parse(request.getHeader("User-Agent"));
String browser = ua.getBrowser().toString() + " " + ua.getVersion();
return browser.replace(".0.0.0", "");
}
/**
* 获得当天是周几
*/
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 "";
}
}
@SuppressWarnings({"unchecked", "all"})
public static List<Field> getAllFields(Class clazz, List<Field> fields) {
if (clazz != null) {
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
getAllFields(clazz.getSuperclass(), fields);
}
return fields;
}
}
@@ -0,0 +1,21 @@
package cn.odboy.util;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* 异常工具
*/
public class ThrowableUtil {
/**
* 获取堆栈信息
*/
public static String getStackTrace(Throwable throwable) {
StringWriter sw = new StringWriter();
try (PrintWriter pw = new PrintWriter(sw)) {
throwable.printStackTrace(pw);
return sw.toString();
}
}
}
+40
View File
@@ -0,0 +1,40 @@
<?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>cutejava</artifactId>
<groupId>cn.odboy</groupId>
<version>1.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cutejava-starter</artifactId>
<name>启动模块</name>
<dependencies>
<dependency>
<groupId>cn.odboy</groupId>
<artifactId>cutejava-system</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
<!-- 打包 -->
<build>
<finalName>cutejava</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 跳过单元测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,43 @@
package cn.odboy;
import cn.odboy.annotation.rest.AnonymousGetMapping;
import cn.odboy.util.SpringBeanHolder;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@Api(hidden = true)
@SpringBootApplication
@EnableTransactionManagement
public class AppRun {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(AppRun.class);
springApplication.run(args);
log.info("---------------------------------------------");
log.info("Local: {}", "http://localhost:8000");
log.info("Swagger: {}", "http://localhost:8000/doc.html");
log.info("---------------------------------------------");
}
@Bean
public SpringBeanHolder springContextHolder() {
return new SpringBeanHolder();
}
/**
* 访问首页提示
*
* @return /
*/
@AnonymousGetMapping("/")
public String index() {
return "Backend service started successfully";
}
}
@@ -0,0 +1,8 @@
_____ _ _ _____ _____ _ ___ _ _ ___
/ ___| | | | | |_ _| | ____| | | / | | | / / / |
| | | | | | | | | |__ | | / /| | | | / / / /| |
| | | | | | | | | __| _ | | / / | | | | / / / / | |
| |___ | |_| | | | | |___ | |_| | / / | | | |/ / / / | |
\_____| \_____/ |_| |_____| \_____/ /_/ |_| |___/ /_/ |_|
Spring Boot Version: (${spring-boot.version}),CuteJava Version: (1.6)
@@ -0,0 +1,223 @@
server:
port: 8000
http2:
# 启用 HTTP/2 支持,提升传输效率
enabled: true
compression:
# 启用 GZIP 压缩,减少传输数据量
enabled: true
# 需要压缩的 MIME 类型
mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
# 最小压缩响应大小(字节)
min-response-size: 1024
# 启用优雅停机,并遵守spring.lifecycle.timeout-per-shutdown-phase属性中给出的超时
# GRACEFUL (优雅): 当应用程序以"GRACEFUL"模式关闭时,它不会接受新的请求且会尝试完成所有正在进行的请求和处理,然后才会终止。这种方式适用于那些可能需要一些时间来清理资源或完成正在进行的任务的场景。
# IMMEDIATE(立即): 当应用程序以"IMMEDIATE"模式关闭时,它会立即终止,而不管当前是否有任何活动任务或请求。这种方式适用于那些可以立即停止而不会造成严重问题的情况。
shutdown: graceful
spring:
application:
name: cutejava
lifecycle:
# 采用java.time.Duration格式的值,如果在这个时间内,优雅停机没有停掉应用,超过这个时间就会强制停止应用
timeout-per-shutdown-phase: 30s
freemarker:
# 是否检查模板位置是否存在
check-template-location: false
# 模板编码
charset: utf-8
cache:
# 解决缓存异常 https://blog.csdn.net/jikeyeka/article/details/125804049
type: redis
data:
redis:
repositories:
# 是否启用Redis存储(关闭防止出现 Multiple Spring Data modules found, entering strict repository configuration mode)
enabled: false
# 数据源
datasource:
druid:
db-type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://127.0.0.1:3306/cutejava20250321?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
username: root
password: 123456
# 初始连接数,建议设置为与最小空闲连接数相同
initial-size: 20
# 最小空闲连接数,保持足够的空闲连接以应对请求
min-idle: 20
# 最大连接数,根据并发需求适当增加
max-active: 50
# 获取连接超时时间(毫秒),调整以满足响应时间要求
max-wait: 3000
# 启用KeepAlive机制,保持长连接
keep-alive: true
# 连接有效性检测间隔时间(毫秒),定期检查连接的健康状态
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: true
# 是否在归还到池中前进行检验
test-on-return: false
# 停用 com_ping 探活机制
use-ping-method: false
# 检测连接是否有效
validation-query: SELECT 1
# 配置监控统计
webStatFilter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
filter:
stat:
enabled: true
# 记录慢SQL
log-slow-sql: true
slow-sql-millis: 2000
merge-sql: true
wall:
config:
multi-statement-allow: true
redis:
database: ${REDIS_DB:1}
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
password: ${REDIS_PWD:123456}
# 连接超时时间
timeout: 5000
# 连接池配置
lettuce:
pool:
# 连接池最大连接数
max-active: 30
# 连接池最大阻塞等待时间(毫秒),负值表示没有限制
max-wait: -1
# 连接池中的最大空闲连接数
max-idle: 20
# 连接池中的最小空闲连接数
min-idle: 1
# 持久层
mybatis-plus:
configuration:
# 开启 Mybatis 二级缓存,默认为 true
cache-enabled: false
# 设置本地缓存作用域, Mybatis 一级缓存, 默认为 SESSION
# 同一个 session 相同查询语句不会再次查询数据库
# 微服务中, 建议设置为STATEMENT, 即关闭一级缓存
local-cache-scope: STATEMENT
# 是否开启自动驼峰命名规则(camel case)映射, 即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射
map-underscore-to-camel-case: true
# Sql日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 启动时是否检查 MyBatis XML 文件的存在, 默认不检查
check-config-location: true
# MyBatis Mapper 所对应的 XML 文件位置
# Maven 多模块项目的扫描路径需以 classpath*: 开头 (即加载多个 jar 包下的 XML 文件)
mapper-locations: classpath*:/mapper/**/*.xml
# MyBatis-Plus 全局策略中的 DB 策略配置
global-config:
db-config:
# 逻辑已删除值(逻辑删除下有效)
logic-delete-value: 0
# 逻辑未删除值(逻辑删除下有效)
logic-not-delete-value: 1
# 全局默认主键类型, 这里为自增主键
id-type: auto
# 表名是否使用驼峰转下划线命名,只对表名生效
table-underline: true
# 是否控制台 print mybatis-plus 的 LOGO
banner: true
# 登录
login:
# 是否限制单用户登录
single-login: false
# Redis用户登录缓存配置
user-cache:
# 存活时间/秒
idle-time: 21600
# 验证码
code:
# 验证码类型配置 查看 LoginProperties 类
code-type: arithmetic
# 登录图形验证码有效时间/分钟
expiration: 2
# 验证码高度
width: 111
# 验证码宽度
height: 36
# 内容长度
length: 2
# 字体名称,为空则使用默认字体
font-name:
# 字体大小
font-size: 25
# 鉴权
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: "captcha_code:"
# token 续期检查时间范围(默认30分钟,单位毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期
detect: 1800000
# 续期时间范围,默认1小时,单位毫秒
renew: 3600000
# 是否开启 swagger-ui
swagger:
enabled: true
# 文件存储路径
file:
mac:
path: ~/file/
avatar: ~/avatar/
linux:
path: /home/${spring.application.name}/file/
avatar: /home/${spring.application.name}/avatar/
windows:
path: C:\${spring.application.name}\file\
avatar: C:\${spring.application.name}\avatar\
# 文件大小M
maxSize: 100
avatarMaxSize: 5
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==
@@ -0,0 +1,223 @@
server:
port: 8000
http2:
# 启用 HTTP/2 支持,提升传输效率
enabled: true
compression:
# 启用 GZIP 压缩,减少传输数据量
enabled: true
# 需要压缩的 MIME 类型
mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
# 最小压缩响应大小(字节)
min-response-size: 1024
# 启用优雅停机,并遵守spring.lifecycle.timeout-per-shutdown-phase属性中给出的超时
# GRACEFUL (优雅): 当应用程序以"GRACEFUL"模式关闭时,它不会接受新的请求且会尝试完成所有正在进行的请求和处理,然后才会终止。这种方式适用于那些可能需要一些时间来清理资源或完成正在进行的任务的场景。
# IMMEDIATE(立即): 当应用程序以"IMMEDIATE"模式关闭时,它会立即终止,而不管当前是否有任何活动任务或请求。这种方式适用于那些可以立即停止而不会造成严重问题的情况。
shutdown: graceful
spring:
application:
name: cutejava
lifecycle:
# 采用java.time.Duration格式的值,如果在这个时间内,优雅停机没有停掉应用,超过这个时间就会强制停止应用
timeout-per-shutdown-phase: 30s
freemarker:
# 是否检查模板位置是否存在
check-template-location: false
# 模板编码
charset: utf-8
cache:
# 解决缓存异常 https://blog.csdn.net/jikeyeka/article/details/125804049
type: redis
data:
redis:
repositories:
# 是否启用Redis存储(关闭防止出现 Multiple Spring Data modules found, entering strict repository configuration mode)
enabled: false
# 数据源
datasource:
druid:
db-type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://127.0.0.1:3306/cutejava20250321?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
username: root
password: 123456
# 初始连接数,建议设置为与最小空闲连接数相同
initial-size: 20
# 最小空闲连接数,保持足够的空闲连接以应对请求
min-idle: 20
# 最大连接数,根据并发需求适当增加
max-active: 50
# 获取连接超时时间(毫秒),调整以满足响应时间要求
max-wait: 3000
# 启用KeepAlive机制,保持长连接
keep-alive: true
# 连接有效性检测间隔时间(毫秒),定期检查连接的健康状态
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: true
# 是否在归还到池中前进行检验
test-on-return: false
# 停用 com_ping 探活机制
use-ping-method: false
# 检测连接是否有效
validation-query: SELECT 1
# 配置监控统计
webStatFilter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
filter:
stat:
enabled: true
# 记录慢SQL
log-slow-sql: true
slow-sql-millis: 2000
merge-sql: true
wall:
config:
multi-statement-allow: true
redis:
database: ${REDIS_DB:1}
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
password: ${REDIS_PWD:123456}
# 连接超时时间
timeout: 5000
# 连接池配置
lettuce:
pool:
# 连接池最大连接数
max-active: 30
# 连接池最大阻塞等待时间(毫秒),负值表示没有限制
max-wait: -1
# 连接池中的最大空闲连接数
max-idle: 20
# 连接池中的最小空闲连接数
min-idle: 1
# 持久层
mybatis-plus:
configuration:
# 开启 Mybatis 二级缓存,默认为 true
cache-enabled: false
# 设置本地缓存作用域, Mybatis 一级缓存, 默认为 SESSION
# 同一个 session 相同查询语句不会再次查询数据库
# 微服务中, 建议设置为STATEMENT, 即关闭一级缓存
local-cache-scope: STATEMENT
# 是否开启自动驼峰命名规则(camel case)映射, 即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射
map-underscore-to-camel-case: true
# Sql日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 启动时是否检查 MyBatis XML 文件的存在, 默认不检查
check-config-location: true
# MyBatis Mapper 所对应的 XML 文件位置
# Maven 多模块项目的扫描路径需以 classpath*: 开头 (即加载多个 jar 包下的 XML 文件)
mapper-locations: classpath*:/mapper/**/*.xml
# MyBatis-Plus 全局策略中的 DB 策略配置
global-config:
db-config:
# 逻辑已删除值(逻辑删除下有效)
logic-delete-value: 0
# 逻辑未删除值(逻辑删除下有效)
logic-not-delete-value: 1
# 全局默认主键类型, 这里为自增主键
id-type: auto
# 表名是否使用驼峰转下划线命名,只对表名生效
table-underline: true
# 是否控制台 print mybatis-plus 的 LOGO
banner: true
# 登录
login:
# 是否限制单用户登录
single-login: true
# Redis用户登录缓存配置
user-cache:
# 存活时间/秒
idle-time: 21600
# 验证码
code:
# 验证码类型配置 查看 LoginProperties 类
code-type: arithmetic
# 登录图形验证码有效时间/分钟
expiration: 2
# 验证码高度
width: 111
# 验证码宽度
height: 36
# 内容长度
length: 2
# 字体名称,为空则使用默认字体
font-name:
# 字体大小
font-size: 25
# 鉴权
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: "captcha_code:"
# token 续期检查时间范围(默认30分钟,单位毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期
detect: 1800000
# 续期时间范围,默认1小时,单位毫秒
renew: 3600000
# 是否开启 swagger-ui
swagger:
enabled: true
# 文件存储路径
file:
mac:
path: ~/file/
avatar: ~/avatar/
linux:
path: /home/${spring.application.name}/file/
avatar: /home/${spring.application.name}/avatar/
windows:
path: C:\${spring.application.name}\file\
avatar: C:\${spring.application.name}\avatar\
# 文件大小M
maxSize: 100
avatarMaxSize: 5
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==
@@ -0,0 +1,31 @@
# 配置 quartz 分布式支持, sql 文件在 sql 目录下,需要导入到数据库,并且需要修改 application.yml 文件的 active: dev 配置
spring:
quartz:
# Quartz集群, 数据库表名称一定要不区分大小写!
job-store-type: jdbc
properties:
org:
quartz:
scheduler:
instanceName: ClusteredScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true
clusterCheckinInterval: 20000
useProperties: false
dataSource: myDS
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadsInheritContextClassLoaderOfInitializingThread: true
dataSource:
myDS:
driver: com.mysql.cj.jdbc.Driver
URL: jdbc:mysql://127.0.0.1:3306/cutejava20250321?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
user: root
password: 123456
maxConnections: 5
validationQuery: select 1
@@ -0,0 +1,5 @@
spring:
profiles:
# 激活的环境,如果需要 quartz 分布式支持,需要修改 active: dev,quartz
active: dev,quartz
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds" debug="false">
<contextName>elAdmin</contextName>
<property name="log.charset" value="utf-8"/>
<property name="log.pattern"
value="%contextName- %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}) - %msg%n"/>
<!--输出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
<charset>${log.charset}</charset>
</encoder>
</appender>
<!--普通日志输出到控制台-->
<root level="info">
<appender-ref ref="console"/>
</root>
<!-- Spring 日志级别控制 -->
<logger name="org.springframework" level="warn"/>
<!-- DnsServerAddressStreamProviders调整为ERROR -->
<logger name="io.netty.resolver.dns.DnsServerAddressStreamProviders" level="ERROR"/>
<!-- 设置其他类的日志级别为 ERROR -->
<logger name="org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]" level="ERROR"/>
<logger name="org.springframework.web.servlet.DispatcherServlet" level="ERROR"/>
</configuration>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.quartz.mapper.QuartzJobMapper">
<resultMap id="BaseResultMap" type="cn.odboy.modules.quartz.domain.QuartzJob">
<id column="job_id" property="id" jdbcType="BIGINT"/>
<result column="job_name" property="jobName" jdbcType="VARCHAR"/>
<result column="bean_name" property="beanName" jdbcType="VARCHAR"/>
<result column="method_name" property="methodName" jdbcType="VARCHAR"/>
<result column="params" property="params" jdbcType="VARCHAR"/>
<result column="cron_expression" property="cronExpression" jdbcType="VARCHAR"/>
<result column="is_pause" property="isPause" jdbcType="TINYINT"/>
<result column="person_in_charge" property="personInCharge" jdbcType="VARCHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
<result column="sub_task" property="subTask" jdbcType="VARCHAR"/>
<result column="pause_after_failure" property="pauseAfterFailure" jdbcType="TINYINT"/>
<result column="description" property="description" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="create_by" property="createBy" jdbcType="VARCHAR"/>
<result column="update_by" property="updateBy" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
job_id
, job_name, bean_name, method_name, params, cron_expression, is_pause, person_in_charge, email, sub_task,
pause_after_failure, description, create_time, update_time, create_by, update_by
</sql>
<select id="findAll" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM sys_quartz_job
<where>
<if test="criteria.jobName != null and criteria.jobName != ''">
AND job_name LIKE CONCAT('%',#{criteria.jobName},'%')
</if>
<if test="criteria.createTime != null and criteria.createTime.size() > 0">
AND update_time BETWEEN #{criteria.createTime[0]} AND #{criteria.createTime[1]}
</if>
</where>
ORDER BY job_id DESC
</select>
<select id="findByIsPauseIsFalse" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM sys_quartz_job
WHERE is_pause = 0
</select>
</mapper>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.quartz.mapper.QuartzLogMapper">
<resultMap id="BaseResultMap" type="cn.odboy.modules.quartz.domain.QuartzLog">
<id column="log_id" property="id" jdbcType="BIGINT"/>
<result column="job_name" property="jobName" jdbcType="VARCHAR"/>
<result column="bean_name" property="beanName" jdbcType="VARCHAR"/>
<result column="method_name" property="methodName" jdbcType="VARCHAR"/>
<result column="params" property="params" jdbcType="VARCHAR"/>
<result column="cron_expression" property="cronExpression" jdbcType="VARCHAR"/>
<result column="is_success" property="isSuccess" jdbcType="VARCHAR"/>
<result column="exception_detail" property="exceptionDetail" jdbcType="BIGINT"/>
<result column="time" property="time" jdbcType="BIGINT"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
log_id
, job_name, bean_name, method_name, params, cron_expression, is_success, exception_detail, time,
create_time
</sql>
<select id="findAll" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM sys_quartz_log
<where>
<if test="criteria.jobName != null and criteria.jobName != ''">
AND job_name LIKE CONCAT('%',#{criteria.jobName},'%')
</if>
<if test="criteria.isSuccess != null">
AND is_success = #{criteria.isSuccess}
</if>
<if test="criteria.createTime != null and criteria.createTime.size() > 0">
AND create_time BETWEEN #{criteria.createTime[0]} AND #{criteria.createTime[1]}
</if>
</where>
ORDER BY log_id DESC
</select>
</mapper>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.DeptMapper">
<resultMap id="BaseResultMap" type="cn.odboy.modules.system.domain.Dept">
<id column="dept_id" property="id"/>
<result column="dept_sort" property="deptSort"/>
<result column="name" property="name"/>
<result column="enabled" property="enabled"/>
<result column="pid" property="pid"/>
<result column="sub_count" property="subCount"/>
<result column="create_by" property="createBy"/>
<result column="update_by" property="updateBy"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="BaseResultMap">
dept_id
, name, pid, sub_count, create_time, update_time, create_by, update_by, enabled, dept_sort
</sql>
<select id="findAll" resultMap="BaseResultMap">
select
<include refid="BaseResultMap"/>
from sys_dept
<where>
<if test="criteria.ids != null and criteria.ids.size() > 0">
and dept_id in
<foreach collection="criteria.ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
<if test="criteria.name != null and criteria.name != ''">
and name like concat('%', #{criteria.name}, '%')
</if>
<if test="criteria.enabled != null">
and enabled = #{criteria.enabled}
</if>
<if test="criteria.pid != null">
and pid = #{criteria.pid}
</if>
<if test="criteria.pidIsNull != null">
and pid is null
</if>
<if test="criteria.createTime != null and criteria.createTime.size() != 0">
and create_time between #{criteria.createTime[0]} and #{criteria.createTime[1]}
</if>
</where>
order by dept_sort
</select>
<select id="findByPid" resultMap="BaseResultMap">
select
<include refid="BaseResultMap"/>
from sys_dept
where pid = #{pid}
</select>
<select id="findByPidIsNull" resultMap="BaseResultMap">
select
<include refid="BaseResultMap"/>
from sys_dept
where pid is null
</select>
<select id="findByRoleId" resultType="cn.odboy.modules.system.domain.Dept">
select d.dept_id as id,
d.name,
d.pid,
d.sub_count,
d.create_time,
d.update_time,
d.create_by,
d.update_by,
d.enabled,
d.dept_sort
from sys_dept d,
sys_roles_depts r
where d.dept_id = r.dept_id
and r.role_id = #{roleId}
</select>
</mapper>
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.DictDetailMapper">
<resultMap id="BaseResultMap" type="cn.odboy.modules.system.domain.DictDetail">
<id column="detail_id" property="id" jdbcType="BIGINT"/>
<result column="label" property="label" jdbcType="VARCHAR"/>
<result column="value" property="value" jdbcType="VARCHAR"/>
<result column="dict_sort" property="dictSort" jdbcType="INTEGER"/>
<result column="create_by" property="createBy" jdbcType="VARCHAR"/>
<result column="update_by" property="updateBy" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<association property="dict" javaType="cn.odboy.modules.system.domain.Dict">
<id column="dict_id" property="id" jdbcType="BIGINT"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
</association>
</resultMap>
<sql id="Base_Column_List">
dd.detail_id, dd.label, dd.`value`, dd.`dict_sort`, dd.create_by, dd.update_by, dd.create_time, dd.update_time,
d.dict_id, d.name
</sql>
<select id="findAll" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from sys_dict_detail dd, sys_dict d
where dd.dict_id = d.dict_id
<if test="criteria.dictName != null and criteria.dictName != ''">
and d.name = #{criteria.dictName}
</if>
<if test="criteria.label != null and criteria.label != ''">
and dd.label = #{criteria.label}
</if>
order by dd.dict_sort, dd.dict_id desc
</select>
<select id="findByDictName" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from sys_dict_detail dd, sys_dict d
where dd.dict_id = d.dict_id
<if test="name != null and name != ''">
and d.name = #{name}
</if>
order by dd.dict_sort
</select>
<delete id="deleteByDictBatchIds">
delete from sys_dict_detail
where dict_id in
<foreach collection="dictIds" item="item" index="index" open="(" separator="," close=")">
#{item}
</foreach>
</delete>
</mapper>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.DictMapper">
<resultMap id="BaseResultMap" type="cn.odboy.modules.system.domain.Dict">
<id column="dict_id" property="id" jdbcType="BIGINT"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="description" property="description" jdbcType="VARCHAR"/>
<result column="create_by" property="createBy" jdbcType="VARCHAR"/>
<result column="update_by" property="updateBy" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
d
.
dict_id
, d.name, d.description, d.create_by, d.update_by, d.create_time, d.update_time
</sql>
<select id="findAll" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
from sys_dict d
<where>
<if test="criteria.blurry != null and criteria.blurry != ''">
and (
d.name like concat('%', #{criteria.blurry}, '%')
or d.description like concat('%', #{criteria.blurry}, '%')
)
</if>
</where>
order by d.dict_id desc
</select>
</mapper>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.JobMapper">
<resultMap id="BaseResultMap" type="cn.odboy.modules.system.domain.Job">
<id column="job_id" property="id"/>
<result column="name" property="name"/>
<result column="job_sort" property="jobSort"/>
<result column="enabled" property="enabled"/>
<result column="create_by" property="createBy"/>
<result column="update_by" property="updateBy"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
job_id
, name, job_sort, enabled, create_by, update_by, create_time, update_time
</sql>
<select id="findAll" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from sys_job
<where>
<if test="criteria.name != null and criteria.name != ''">
and name like concat('%', #{criteria.name}, '%')
</if>
<if test="criteria.enabled != null">
and enabled = #{criteria.enabled}
</if>
<if test="criteria.createTime != null and criteria.createTime.size() != 0">
and create_time between #{criteria.createTime[0]} and #{criteria.createTime[1]}
</if>
</where>
order by job_sort, job_id desc
</select>
</mapper>
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.MenuMapper">
<resultMap id="BaseResultMap" type="cn.odboy.modules.system.domain.Menu">
<id column="menu_id" property="id"/>
<result column="title" property="title"/>
<result column="name" property="componentName"/>
<result column="menu_sort" property="menuSort"/>
<result column="component" property="component"/>
<result column="path" property="path"/>
<result column="type" property="type"/>
<result column="permission" property="permission"/>
<result column="icon" property="icon"/>
<result column="cache" property="cache"/>
<result column="hidden" property="hidden"/>
<result column="pid" property="pid"/>
<result column="sub_count" property="subCount"/>
<result column="i_frame" property="iFrame"/>
<result column="create_by" property="createBy"/>
<result column="update_by" property="updateBy"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
menu_id, title, name, menu_sort, component, path, type, permission, icon, cache, hidden, pid, sub_count,
i_frame, create_by, update_by, create_time, update_time
</sql>
<select id="findAll" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from sys_menu
<where>
<if test="criteria.blurry != null and criteria.blurry != ''">
and (
title like concat('%',#{criteria.blurry},'%')
or name like concat('%',#{criteria.blurry},'%')
or permission like concat('%',#{criteria.blurry},'%')
)
</if>
<if test="criteria.pidIsNull != null">
and pid is null
</if>
<if test="criteria.pid != null">
and pid = #{criteria.pid}
</if>
<if test="criteria.createTime != null and criteria.createTime.size() != 0">
and create_time between #{criteria.createTime[0]} and #{criteria.createTime[1]}
</if>
</where>
order by menu_sort
</select>
<select id="findByRoleIdsAndTypeNot" resultMap="BaseResultMap">
SELECT m.* FROM sys_menu m, sys_roles_menus r
WHERE m.menu_id = r.menu_id AND r.role_id IN
<foreach collection="roleIds" item="roleId" open="(" separator="," close=")">
#{roleId}
</foreach>
AND type != #{type}
order by m.menu_sort
</select>
<select id="findByPidOrderByMenuSort" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
from sys_menu
WHERE pid = #{pid}
ORDER BY menu_sort
</select>
<select id="findByPidIsNullOrderByMenuSort" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
from sys_menu
where pid is null
order by menu_sort
</select>
</mapper>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.RoleDeptMapper">
<insert id="insertData">
insert into sys_roles_depts (role_id, dept_id)
values
<foreach collection="depts" item="item" open="(" separator="),(" close=")">
#{roleId}, #{item.id}
</foreach>
</insert>
<delete id="deleteByRoleId">
delete from sys_roles_depts
where role_id = #{roleId}
</delete>
<delete id="deleteByRoleIds">
delete from sys_roles_depts
where role_id in
<foreach collection="roleIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.RoleMapper">
<resultMap id="BaseResultMap" type="cn.odboy.modules.system.domain.Role">
<id column="role_role_id" property="id"/>
<result column="role_name" property="name"/>
<result column="role_data_scope" property="dataScope"/>
<result column="role_level" property="level"/>
<result column="role_description" property="description"/>
<result column="role_create_by" property="createBy"/>
<result column="role_update_by" property="updateBy"/>
<result column="role_create_time" property="createTime"/>
<result column="role_update_time" property="updateTime"/>
<collection property="menus" ofType="cn.odboy.modules.system.domain.Menu">
<id column="menu_id" property="id"/>
<result column="menu_title" property="title"/>
<result column="menu_permission" property="permission"/>
</collection>
<collection property="depts" ofType="cn.odboy.modules.system.domain.Dept">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
</collection>
</resultMap>
<sql id="Base_Column_List">
role.role_id as role_role_id, role.name as role_name, role.data_scope as role_data_scope,
role.level as role_level, role.description as role_description, role.create_by as role_create_by,
role.update_by as role_update_by, role.create_time as role_create_time, role.update_time as role_update_time
</sql>
<sql id="Menu_Column_List">
menu.menu_id as menu_id, menu.title as menu_title, menu.permission as menu_permission
</sql>
<sql id="Dept_Column_List">
dept.dept_id as dept_id, dept.name as dept_name
</sql>
<sql id="Where_sql">
<where>
<if test="criteria.blurry != null and criteria.blurry != ''">
and (
role.name like concat('%', #{criteria.blurry}, '%')
or role.description like concat('%', #{criteria.blurry}, '%')
)
</if>
<if test="criteria.createTime != null and criteria.createTime.size() != 0">
and role.create_time between #{criteria.createTime[0]} and #{criteria.createTime[1]}
</if>
</where>
</sql>
<select id="queryAll" resultType="cn.odboy.modules.system.domain.Role">
select role_id as id, name, level
from sys_role order by level
</select>
<select id="findAll" resultMap="BaseResultMap">
select role.*,
<include refid="Dept_Column_List"/>,
<include refid="Menu_Column_List"/>
from (
select
<include refid="Base_Column_List"/>
from sys_role role
<include refid="Where_sql"/>
order by role.level
<if test="criteria.offset != null">
limit #{criteria.offset}, #{criteria.size}
</if>
) role
left join sys_roles_menus srm on role.role_role_id = srm.role_id
left join sys_menu menu on menu.menu_id = srm.menu_id
left join sys_roles_depts srd on role.role_role_id = srd.role_id
left join sys_dept dept on dept.dept_id = srd.dept_id
order by role.role_level
</select>
<select id="countAll" resultType="java.lang.Long">
select count(*)
from sys_role role
<include refid="Where_sql"/>
</select>
<select id="findByName" resultType="cn.odboy.modules.system.domain.Role">
select role_id as id from sys_role
where name = #{name}
</select>
<select id="findById" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>,
<include refid="Dept_Column_List"/>,
<include refid="Menu_Column_List"/>
from sys_role role
left join sys_roles_menus srm on role.role_id = srm.role_id
left join sys_menu menu on menu.menu_id = srm.menu_id
left join sys_roles_depts srd on role.role_id = srd.role_id
left join sys_dept dept on dept.dept_id = srd.dept_id
where role.role_id = #{roleId}
</select>
<select id="findByUserId" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>,
<include refid="Dept_Column_List"/>,
<include refid="Menu_Column_List"/>
from sys_role role
left join sys_roles_menus srm on role.role_id = srm.role_id
left join sys_menu menu on menu.menu_id = srm.menu_id
left join sys_roles_depts srd on role.role_id = srd.role_id
left join sys_dept dept on dept.dept_id = srd.dept_id
left join sys_users_roles ur on role.role_id = ur.role_id
WHERE role.role_id = ur.role_id AND ur.user_id = #{userId}
</select>
<select id="countByDepts" resultType="int">
select count(*)
from sys_role r, sys_roles_depts d
where r.role_id = d.role_id
and d.dept_id in
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
#{deptId}
</foreach>
</select>
</mapper>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.RoleMenuMapper">
<insert id="insertData">
insert into sys_roles_menus (role_id, menu_id)
values
<foreach collection="menus" item="item" open="(" separator="),(" close=")">
#{roleId}, #{item.id}
</foreach>
</insert>
<delete id="deleteByRoleId">
delete from sys_roles_menus
where role_id = #{roleId}
</delete>
<delete id="deleteByRoleIds">
delete from sys_roles_menus
where role_id in
<foreach collection="roleIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
<delete id="deleteByMenuId">
delete from sys_roles_menus
where menu_id = #{menuId}
</delete>
</mapper>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.UserJobMapper">
<insert id="insertData">
insert into sys_users_jobs (user_id, job_id)
values
<foreach collection="jobs" item="item" open="(" separator="),(" close=")">
#{userId}, #{item.id}
</foreach>
</insert>
<delete id="deleteByUserId">
delete from sys_users_jobs
where user_id = #{userId}
</delete>
<delete id="deleteByUserIds">
delete from sys_users_jobs
where user_id in
<foreach collection="userIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.UserMapper">
<resultMap id="BaseResultMap" type="cn.odboy.modules.system.domain.User">
<id column="user_user_id" property="id"/>
<result column="user_dept_id" property="deptId"/>
<result column="user_username" property="username"/>
<result column="user_nick_name" property="nickName"/>
<result column="user_email" property="email"/>
<result column="user_phone" property="phone"/>
<result column="user_gender" property="gender"/>
<result column="user_avatar_name" property="avatarName"/>
<result column="user_avatar_path" property="avatarPath"/>
<result column="user_password" property="password"/>
<result column="user_is_admin" property="isAdmin"/>
<result column="user_enabled" property="enabled"/>
<result column="user_pwd_reset_time" property="pwdResetTime"/>
<result column="user_create_by" property="createBy"/>
<result column="user_update_by" property="updateBy"/>
<result column="user_create_time" property="createTime"/>
<result column="user_update_time" property="updateTime"/>
<association property="dept" javaType="cn.odboy.modules.system.domain.Dept">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
</association>
<collection property="jobs" ofType="cn.odboy.modules.system.domain.Job">
<id column="job_id" property="id"/>
<result column="job_name" property="name"/>
</collection>
<collection property="roles" ofType="cn.odboy.modules.system.domain.Role">
<id column="role_id" property="id"/>
<result column="role_name" property="name"/>
<result column="role_level" property="level"/>
<result column="role_data_scope" property="dataScope"/>
</collection>
</resultMap>
<sql id="Base_Column_List">
u.user_id as user_user_id, u.dept_id as user_dept_id, u.username as user_username,
u.nick_name as user_nick_name, u.email as user_email, u.phone as user_phone,
u.gender as user_gender, u.avatar_name as user_avatar_name, u.avatar_path as user_avatar_path,
u.enabled as user_enabled, u.pwd_reset_time as user_pwd_reset_time, u.create_by as user_create_by,
u.update_by as user_update_by, u.create_time as user_create_time, u.update_time as user_update_time,
d.dept_id as dept_id, d.name as dept_name
</sql>
<sql id="Job_Column_List">
j.job_id as job_id, j.name as job_name
</sql>
<sql id="Role_Column_List">
r.role_id as role_id, r.name as role_name, r.level as role_level, r.data_scope as role_data_scope
</sql>
<sql id="Whrer_Sql">
<where>
<if test="criteria.id != null">
and u.user_id = #{criteria.id}
</if>
<if test="criteria.enabled != null">
and u.enabled = #{criteria.enabled}
</if>
<if test="criteria.deptIds != null and criteria.deptIds.size() != 0">
and u.dept_id in
<foreach collection="criteria.deptIds" item="deptId" open="(" separator="," close=")">
#{deptId}
</foreach>
</if>
<if test="criteria.blurry != null and criteria.blurry != ''">
and (
u.username like concat('%', #{criteria.blurry}, '%')
or u.nick_name like concat('%', #{criteria.blurry}, '%')
or u.email like concat('%', #{criteria.blurry}, '%')
)
</if>
<if test="criteria.createTime != null and criteria.createTime.size() != 0">
and u.create_time between #{criteria.createTime[0]} and #{criteria.createTime[1]}
</if>
</where>
</sql>
<select id="findAll" resultMap="BaseResultMap">
select u.*,
<include refid="Job_Column_List"/>,
<include refid="Role_Column_List"/>
from (
select
<include refid="Base_Column_List"/>
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
<include refid="Whrer_Sql"/>
order by u.user_id desc
<if test="criteria.offset != null">
limit #{criteria.offset}, #{criteria.size}
</if>
) u
left join sys_users_jobs suj on u.user_user_id = suj.user_id
left join sys_job j on suj.job_id = j.job_id
left join sys_users_roles sur on u.user_user_id = sur.user_id
left join sys_role r on sur.role_id = r.role_id
order by u.user_user_id desc
</select>
<select id="countAll" resultType="java.lang.Long">
select count(*)
from sys_user u
<include refid="Whrer_Sql"/>
</select>
<select id="findByUsername" resultMap="BaseResultMap">
select
u.password user_password, u.is_admin user_is_admin,
<include refid="Base_Column_List"/>
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.username = #{username}
</select>
<select id="findByEmail" resultType="cn.odboy.modules.system.domain.User">
select user_id as id, username from sys_user
where email = #{email}
</select>
<select id="findByPhone" resultType="cn.odboy.modules.system.domain.User">
select user_id as id, username from sys_user
where phone = #{phone}
</select>
<select id="findByRoleId" resultType="cn.odboy.modules.system.domain.User">
SELECT u.user_id as id, u.username FROM sys_user u, sys_users_roles r
WHERE u.user_id = r.user_id AND r.role_id = #{roleId}
group by u.user_id
</select>
<select id="findByRoleDeptId" resultType="cn.odboy.modules.system.domain.User">
SELECT u.* FROM sys_user u, sys_users_roles r, sys_roles_depts d
WHERE u.user_id = r.user_id AND r.role_id = d.role_id AND d.dept_id = #{deptId}
group by u.user_id
</select>
<select id="findByMenuId" resultType="cn.odboy.modules.system.domain.User">
SELECT u.user_id as id, u.username FROM sys_user u, sys_users_roles ur, sys_roles_menus rm
WHERE u.user_id = ur.user_id AND ur.role_id = rm.role_id AND rm.menu_id = #{menuId}
group by u.user_id
</select>
<select id="countByJobs" resultType="int">
SELECT count(*) FROM sys_user u, sys_users_jobs j
WHERE u.user_id = j.user_id AND j.job_id IN
<foreach collection="jobIds" item="jobId" open="(" separator="," close=")">
#{jobId}
</foreach>
</select>
<select id="countByDepts" resultType="int">
SELECT count(*) FROM sys_user u
WHERE u.dept_id IN
<foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
#{deptId}
</foreach>
</select>
<select id="countByRoles" resultType="int">
SELECT count(*) FROM sys_user u, sys_users_roles r
WHERE u.user_id = r.user_id AND r.role_id in
<foreach collection="roleIds" item="roleId" open="(" separator="," close=")">
#{roleId}
</foreach>
</select>
<update id="resetPwd">
update sys_user set password = #{pwd}
where user_id in
<foreach collection="userIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
</mapper>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.odboy.modules.system.mapper.UserRoleMapper">
<insert id="insertData">
insert into sys_users_roles (user_id, role_id)
values
<foreach collection="roles" item="item" open="(" separator="),(" close=")">
#{userId}, #{item.id}
</foreach>
</insert>
<delete id="deleteByUserId">
delete from sys_users_roles
where user_id = #{userId}
</delete>
<delete id="deleteByUserIds">
delete from sys_users_roles
where user_id in
<foreach collection="userIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
@@ -0,0 +1,20 @@
# 应用的拦截模块
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=cn.odboy.config.mybatis.CustomP6SpyLogger
# 日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动 可多个
driverlist=com.mysql.cj.jdbc.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
# 是否过滤 Log
filter=true
# 过滤 Log 时所排除的 sql 关键字,以逗号分隔
exclude=SELECT 1,INSERT INTO sys_log
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
@@ -0,0 +1,50 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style>
@page {
margin: 0;
}
</style>
</head>
<body style="margin: 0px;
padding: 0px;
font: 100% SimSun, Microsoft YaHei, Times New Roman, Verdana, Arial, Helvetica, sans-serif;
color: #000;">
<div style="height: auto;
width: 820px;
min-width: 820px;
margin: 0 auto;
margin-top: 20px;
border: 1px solid #eee;">
<div style="padding: 10px;padding-bottom: 0px;">
<p style="margin-bottom: 10px;padding-bottom: 0px;">尊敬的用户,您好:</p>
<p style="text-indent: 2em; margin-bottom: 10px;">您正在申请邮箱验证,您的验证码为:</p>
<p style="text-align: center;
font-family: Times New Roman;
font-size: 22px;
color: #C60024;
padding: 20px 0px;
margin-bottom: 10px;
font-weight: bold;
background: #ebebeb;">${code}</p>
<div class="foot-hr hr" style="margin: 0 auto;
z-index: 111;
width: 800px;
margin-top: 30px;
border-top: 1px solid #DA251D;">
</div>
<div style="text-align: center;
font-size: 12px;
padding: 20px 0px;
font-family: Microsoft YaHei;">
Copyright &copy;${.now?string("yyyy")} <a hover="color: #DA251D;" style="color: #999;"
href="https://github.com/odboy-tianjun/cutejava" target="_blank">CuteJava</a>
后台管理系统 All Rights Reserved.
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,71 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style>
@page {
margin: 0;
}
</style>
</head>
<body style="margin: 0px;
padding: 0px;
font: 100% SimSun, Microsoft YaHei, Times New Roman, Verdana, Arial, Helvetica, sans-serif;
color: #000;">
<div style="height: auto;
margin: 0 auto;
margin-top: 20px;
padding: 20px;
border: 1px solid #eee;">
<div>
<p style="margin-bottom: 10px;">任务信息:</p>
<table style="border-collapse: collapse;">
<tr>
<th style="padding: .65em;background: #666;border: 1px solid #777;color: #fff;">任务名称</th>
<th style="padding: .65em;background: #666;border: 1px solid #777;color: #fff;">Bean名称</th>
<th style="padding: .65em;background: #666;border: 1px solid #777;color: #fff;">执行方法</th>
<th style="padding: .65em;background: #666;border: 1px solid #777;color: #fff;">参数内容</th>
<th style="padding: .65em;background: #666;border: 1px solid #777;color: #fff;">Cron表达式</th>
<th style="padding: .65em;background: #666;border: 1px solid #777;color: #fff;">描述内容</th>
</tr>
<tr>
<td style="padding: .65em;border: 1px solid#777;">${task.jobName}</td>
<td style="padding: .65em;border: 1px solid#777;">${task.beanName}</td>
<td style="padding: .65em;border: 1px solid#777;">${task.methodName}</td>
<td style="padding: .65em;border: 1px solid#777;">${(task.params)!""}</td>
<td style="padding: .65em;border: 1px solid#777;">${task.cronExpression}</td>
<td style="padding: .65em;border: 1px solid#777;">${(task.description)!""}</td>
</tr>
</table>
</div>
<div>
<p style="margin-bottom: 10px;">异常信息:</p>
<pre style="position: relative;
padding: 15px;
line-height: 20px;
border-left: 5px solid #ddd;
color: #333;
font-family: Courier New, serif;
font-size: 12px">
${msg}
</pre>
</div>
<div class="foot-hr hr" style="margin: 0 auto;
z-index: 111;
width: 800px;
margin-top: 30px;
border-top: 1px solid #DA251D;">
</div>
<div style="text-align: center;
font-size: 12px;
padding: 20px 0px;
font-family: Microsoft YaHei;">
Copyright &copy;${.now?string("yyyy")} <a hover="color: #DA251D;" style="color: #999;"
href="https://github.com/odboy-tianjun/cutejava" target="_blank">ELADMIN</a>
后台管理系统 All Rights Reserved.
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,26 @@
package cn.odboy.util;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Date;
public class DateUtilsTest {
@Test
public void test1() {
long l = System.currentTimeMillis() / 1000;
LocalDateTime localDateTime = DateUtil.fromTimeStamp(l);
System.out.print(DateUtil.localDateTimeFormatyMdHms(localDateTime));
}
@Test
public void test2() {
LocalDateTime now = LocalDateTime.now();
System.out.println(DateUtil.localDateTimeFormatyMdHms(now));
Date date = DateUtil.toDate(now);
LocalDateTime localDateTime = DateUtil.toLocalDateTime(date);
System.out.println(DateUtil.localDateTimeFormatyMdHms(localDateTime));
LocalDateTime localDateTime1 = DateUtil.fromTimeStamp(date.getTime() / 1000);
System.out.println(DateUtil.localDateTimeFormatyMdHms(localDateTime1));
}
}
@@ -0,0 +1,40 @@
package cn.odboy.util;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import static cn.odboy.util.EncryptUtil.desDecrypt;
import static cn.odboy.util.EncryptUtil.desEncrypt;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class EncryptUtilTest {
@SneakyThrows
public static void main(String[] args) {
System.err.println(desEncrypt("abc12345678"));
}
/**
* 对称加密
*/
@Test
public void testDesEncrypt() {
try {
assertEquals("7772841DC6099402", desEncrypt("abc12345678"));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 对称解密
*/
@Test
public void testDesDecrypt() {
try {
assertEquals("abc12345678", desDecrypt("7772841DC6099402"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,35 @@
package cn.odboy.util;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class FileUtilTest {
@Test
public void testToFile() {
long retval = toFile(new MockMultipartFile("foo", (byte[]) null)).getTotalSpace();
assertEquals(500695072768L, retval);
}
@Test
public void testGetExtensionName() {
assertEquals("foo", getExtensionName("foo"));
assertEquals("exe", getExtensionName("bar.exe"));
}
@Test
public void testGetFileNameNoEx() {
assertEquals("foo", getFileNameNoEx("foo"));
assertEquals("bar", getFileNameNoEx("bar.txt"));
}
@Test
public void testGetSize() {
assertEquals("1000B ", getSize(1000));
assertEquals("1.00KB ", getSize(1024));
assertEquals("1.00MB ", getSize(1048576));
assertEquals("1.00GB ", getSize(1073741824));
}
}
@@ -0,0 +1,48 @@
package cn.odboy.util;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
import static cn.odboy.util.StringUtil.getIp;
import static cn.odboy.util.StringUtil.getWeekDay;
import static cn.odboy.util.StringUtil.toCamelCase;
import static cn.odboy.util.StringUtil.toCapitalizeCamelCase;
import static cn.odboy.util.StringUtil.toUnderScoreCase;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class StringUtilTest {
@Test
public void testToCamelCase() {
assertNull(toCamelCase(null));
}
@Test
public void testToCapitalizeCamelCase() {
assertNull(StringUtil.toCapitalizeCamelCase(null));
assertEquals("HelloWorld", toCapitalizeCamelCase("hello_world"));
}
@Test
public void testToUnderScoreCase() {
assertNull(StringUtil.toUnderScoreCase(null));
assertEquals("hello_world", toUnderScoreCase("helloWorld"));
assertEquals("\u0000\u0000", toUnderScoreCase("\u0000\u0000"));
assertEquals("\u0000_a", toUnderScoreCase("\u0000A"));
}
@Test
public void testGetWeekDay() {
SimpleDateFormat simpleDateformat = new SimpleDateFormat("E");
assertEquals(simpleDateformat.format(new Date()), getWeekDay());
}
@Test
public void testGetIP() {
assertEquals("127.0.0.1", getIp(new MockHttpServletRequest()));
}
}
@@ -0,0 +1,16 @@
package cn.odboy.util;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SystemApplicationTests {
@Test
public void contextLoads() {
}
public static void main(String[] args) {
}
}
+79
View File
@@ -0,0 +1,79 @@
<?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>cutejava</artifactId>
<groupId>cn.odboy</groupId>
<version>1.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cutejava-system</artifactId>
<name>核心模块</name>
<properties>
<jjwt.version>0.11.5</jjwt.version>
<!-- oshi监控需要指定jna版本, 问题详见 https://github.com/oshi/oshi/issues/1040 -->
<jna.version>5.8.0</jna.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.odboy</groupId>
<artifactId>cutejava-tools</artifactId>
<version>1.3</version>
</dependency>
<!-- quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.10.1</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
</dependency>
<!-- linux的管理 -->
<dependency>
<groupId>ch.ethz.ganymed</groupId>
<artifactId>ganymed-ssh2</artifactId>
<version>build210</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<!-- 获取系统信息 -->
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.6.5</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,33 @@
package cn.odboy.modules.quartz.config;
import cn.odboy.modules.quartz.domain.QuartzJob;
import cn.odboy.modules.quartz.mapper.QuartzJobMapper;
import cn.odboy.modules.quartz.util.QuartzManage;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@RequiredArgsConstructor
public class JobRunner implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(JobRunner.class);
private final QuartzJobMapper quartzJobMapper;
private final QuartzManage quartzManage;
/**
* 项目启动时重新激活启用的定时任务
*
* @param applicationArguments /
*/
@Override
public void run(ApplicationArguments applicationArguments) {
List<QuartzJob> quartzJobs = quartzJobMapper.findByIsPauseIsFalse();
quartzJobs.forEach(quartzManage::addJob);
log.info("Timing task injection complete");
}
}
@@ -0,0 +1,49 @@
package cn.odboy.modules.quartz.config;
import lombok.extern.slf4j.Slf4j;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
/**
* 定时任务配置
*/
@Slf4j
@Configuration
@Scope("singleton")
public class QuartzConfig {
/**
* 解决Job中注入Spring Bean为null的问题
*/
@Component("quartzJobFactory")
public static class QuartzJobFactory extends AdaptableJobFactory {
private final AutowireCapableBeanFactory capableBeanFactory;
@Autowired
public QuartzJobFactory(AutowireCapableBeanFactory capableBeanFactory) {
this.capableBeanFactory = capableBeanFactory;
}
@NonNull
@Override
protected Object createJobInstance(@NonNull TriggerFiredBundle bundle) throws Exception {
try {
// 调用父类的方法,把Job注入到spring中
Object jobInstance = super.createJobInstance(bundle);
capableBeanFactory.autowireBean(jobInstance);
log.debug("Job instance created and autowired: {}", jobInstance.getClass().getName());
return jobInstance;
} catch (Exception e) {
log.error("Error creating job instance for bundle: {}", bundle, e);
throw e;
}
}
}
}
@@ -0,0 +1,68 @@
package cn.odboy.modules.quartz.domain;
import cn.odboy.base.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Getter
@Setter
@TableName("sys_quartz_job")
public class QuartzJob extends BaseEntity implements Serializable {
public static final String JOB_KEY = "JOB_KEY";
@TableId(value = "job_id", type = IdType.AUTO)
@NotNull(groups = {Update.class})
private Long id;
@TableField(exist = false)
@ApiModelProperty(value = "用于子任务唯一标识", hidden = true)
private String uuid;
@ApiModelProperty(value = "定时器名称")
private String jobName;
@NotBlank
@ApiModelProperty(value = "Bean名称")
private String beanName;
@NotBlank
@ApiModelProperty(value = "方法名称")
private String methodName;
@ApiModelProperty(value = "参数")
private String params;
@NotBlank
@ApiModelProperty(value = "cron表达式")
private String cronExpression;
@ApiModelProperty(value = "状态,暂时或启动")
private Boolean isPause = false;
@ApiModelProperty(value = "负责人")
private String personInCharge;
@ApiModelProperty(value = "报警邮箱")
private String email;
@ApiModelProperty(value = "子任务")
private String subTask;
@ApiModelProperty(value = "失败后暂停")
private Boolean pauseAfterFailure;
@NotBlank
@ApiModelProperty(value = "备注")
private String description;
}
@@ -0,0 +1,45 @@
package cn.odboy.modules.quartz.domain;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.sql.Timestamp;
@Data
@TableName("sys_quartz_log")
public class QuartzLog implements Serializable {
@TableId(value = "log_id", type = IdType.AUTO)
@ApiModelProperty(value = "ID", hidden = true)
private Long id;
@ApiModelProperty(value = "任务名称", hidden = true)
private String jobName;
@ApiModelProperty(value = "bean名称", hidden = true)
private String beanName;
@ApiModelProperty(value = "方法名称", hidden = true)
private String methodName;
@ApiModelProperty(value = "参数", hidden = true)
private String params;
@ApiModelProperty(value = "cron表达式", hidden = true)
private String cronExpression;
@ApiModelProperty(value = "状态", hidden = true)
private Boolean isSuccess;
@ApiModelProperty(value = "异常详情", hidden = true)
private String exceptionDetail;
@ApiModelProperty(value = "执行耗时", hidden = true)
private Long time;
@TableField(fill = FieldFill.INSERT)
@ApiModelProperty(value = "创建时间", hidden = true)
private Timestamp createTime;
}
@@ -0,0 +1,27 @@
package cn.odboy.modules.quartz.domain.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.sql.Timestamp;
import java.util.List;
@Data
public class QuartzJobQueryCriteria {
@ApiModelProperty(value = "定时任务名称")
private String jobName;
@ApiModelProperty(value = "是否成功")
private Boolean isSuccess;
@ApiModelProperty(value = "创建时间")
private List<Timestamp> createTime;
@ApiModelProperty(value = "页码", example = "1")
private Integer page = 1;
@ApiModelProperty(value = "每页数据量", example = "10")
private Integer size = 10;
}
@@ -0,0 +1,21 @@
package cn.odboy.modules.quartz.mapper;
import cn.odboy.modules.quartz.domain.QuartzJob;
import cn.odboy.modules.quartz.domain.dto.QuartzJobQueryCriteria;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface QuartzJobMapper extends BaseMapper<QuartzJob> {
IPage<QuartzJob> findAll(@Param("criteria") QuartzJobQueryCriteria criteria, Page<Object> page);
List<QuartzJob> findAll(@Param("criteria") QuartzJobQueryCriteria criteria);
List<QuartzJob> findByIsPauseIsFalse();
}
@@ -0,0 +1,20 @@
package cn.odboy.modules.quartz.mapper;
import cn.odboy.modules.quartz.domain.QuartzLog;
import cn.odboy.modules.quartz.domain.dto.QuartzJobQueryCriteria;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface QuartzLogMapper extends BaseMapper<QuartzLog> {
IPage<QuartzLog> findAll(@Param("criteria") QuartzJobQueryCriteria criteria, Page<Object> page);
List<QuartzLog> findAll(@Param("criteria") QuartzJobQueryCriteria criteria);
}
@@ -0,0 +1,125 @@
package cn.odboy.modules.quartz.rest;
import cn.odboy.exception.BadRequestException;
import cn.odboy.modules.quartz.domain.QuartzJob;
import cn.odboy.modules.quartz.domain.QuartzLog;
import cn.odboy.modules.quartz.domain.dto.QuartzJobQueryCriteria;
import cn.odboy.modules.quartz.service.QuartzJobService;
import cn.odboy.util.PageResult;
import cn.odboy.util.SpringBeanHolder;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/jobs")
@Api(tags = "系统:定时任务管理")
public class QuartzJobController {
private static final String ENTITY_NAME = "quartzJob";
private final QuartzJobService quartzJobService;
@ApiOperation("查询定时任务")
@GetMapping
@PreAuthorize("@el.check('timing:list')")
public ResponseEntity<PageResult<QuartzJob>> queryQuartzJob(QuartzJobQueryCriteria criteria) {
Page<Object> page = new Page<>(criteria.getPage(), criteria.getSize());
return new ResponseEntity<>(quartzJobService.queryAll(criteria, page), HttpStatus.OK);
}
@ApiOperation("导出任务数据")
@GetMapping(value = "/download")
@PreAuthorize("@el.check('timing:list')")
public void exportQuartzJob(HttpServletResponse response, QuartzJobQueryCriteria criteria) throws IOException {
quartzJobService.download(quartzJobService.queryAll(criteria), response);
}
@ApiOperation("导出日志数据")
@GetMapping(value = "/logs/download")
@PreAuthorize("@el.check('timing:list')")
public void exportQuartzJobLog(HttpServletResponse response, QuartzJobQueryCriteria criteria) throws IOException {
quartzJobService.downloadLog(quartzJobService.queryAllLog(criteria), response);
}
@ApiOperation("查询任务执行日志")
@GetMapping(value = "/logs")
@PreAuthorize("@el.check('timing:list')")
public ResponseEntity<PageResult<QuartzLog>> queryQuartzJobLog(QuartzJobQueryCriteria criteria) {
Page<Object> page = new Page<>(criteria.getPage(), criteria.getSize());
return new ResponseEntity<>(quartzJobService.queryAllLog(criteria, page), HttpStatus.OK);
}
@ApiOperation("新增定时任务")
@PostMapping
@PreAuthorize("@el.check('timing:add')")
public ResponseEntity<Object> createQuartzJob(@Validated @RequestBody QuartzJob resources) {
if (resources.getId() != null) {
throw new BadRequestException("A new " + ENTITY_NAME + " cannot already have an ID");
}
// 验证Bean是不是合法的,合法的定时任务 Bean 需要用 @Service 定义
checkBean(resources.getBeanName());
quartzJobService.create(resources);
return new ResponseEntity<>(HttpStatus.CREATED);
}
@ApiOperation("修改定时任务")
@PutMapping
@PreAuthorize("@el.check('timing:edit')")
public ResponseEntity<Object> updateQuartzJob(@Validated(QuartzJob.Update.class) @RequestBody QuartzJob resources) {
// 验证Bean是不是合法的,合法的定时任务 Bean 需要用 @Service 定义
checkBean(resources.getBeanName());
quartzJobService.update(resources);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@ApiOperation("更改定时任务状态")
@PutMapping(value = "/{id}")
@PreAuthorize("@el.check('timing:edit')")
public ResponseEntity<Object> updateQuartzJobStatus(@PathVariable Long id) {
quartzJobService.updateIsPause(quartzJobService.getById(id));
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@ApiOperation("执行定时任务")
@PutMapping(value = "/exec/{id}")
@PreAuthorize("@el.check('timing:edit')")
public ResponseEntity<Object> executionQuartzJob(@PathVariable Long id) {
quartzJobService.execution(quartzJobService.getById(id));
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@ApiOperation("删除定时任务")
@DeleteMapping
@PreAuthorize("@el.check('timing:del')")
public ResponseEntity<Object> deleteQuartzJob(@RequestBody Set<Long> ids) {
quartzJobService.delete(ids);
return new ResponseEntity<>(HttpStatus.OK);
}
/**
* 验证Bean是不是合法的,合法的定时任务 Bean 需要用 @Service 定义
*
* @param beanName Bean名称
*/
private void checkBean(String beanName) {
// 避免调用攻击者可以从SpringContextHolder获得控制jdbcTemplate类
// 并使用getDeclaredMethod调用jdbcTemplate的queryForMap函数,执行任意sql命令。
if (!SpringBeanHolder.getAllServiceBeanName().contains(beanName)) {
throw new BadRequestException("非法的 Bean,请重新输入!");
}
}
}
@@ -0,0 +1,112 @@
package cn.odboy.modules.quartz.service;
import cn.odboy.modules.quartz.domain.QuartzJob;
import cn.odboy.modules.quartz.domain.QuartzLog;
import cn.odboy.modules.quartz.domain.dto.QuartzJobQueryCriteria;
import cn.odboy.util.PageResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Set;
public interface QuartzJobService extends IService<QuartzJob> {
/**
* 分页查询
*
* @param criteria 条件
* @param page 分页参数
* @return /
*/
PageResult<QuartzJob> queryAll(QuartzJobQueryCriteria criteria, Page<Object> page);
/**
* 查询全部
*
* @param criteria 条件
* @return /
*/
List<QuartzJob> queryAll(QuartzJobQueryCriteria criteria);
/**
* 分页查询日志
*
* @param criteria 条件
* @param page 分页参数
* @return /
*/
PageResult<QuartzLog> queryAllLog(QuartzJobQueryCriteria criteria, Page<Object> page);
/**
* 查询全部
*
* @param criteria 条件
* @return /
*/
List<QuartzLog> queryAllLog(QuartzJobQueryCriteria criteria);
/**
* 创建
*
* @param resources /
*/
void create(QuartzJob resources);
/**
* 编辑
*
* @param resources /
*/
void update(QuartzJob resources);
/**
* 删除任务
*
* @param ids /
*/
void delete(Set<Long> ids);
/**
* 更改定时任务状态
*
* @param quartzJob /
*/
void updateIsPause(QuartzJob quartzJob);
/**
* 立即执行定时任务
*
* @param quartzJob /
*/
void execution(QuartzJob quartzJob);
/**
* 导出定时任务
*
* @param queryAll 待导出的数据
* @param response /
* @throws IOException /
*/
void download(List<QuartzJob> queryAll, HttpServletResponse response) throws IOException;
/**
* 导出定时任务日志
*
* @param queryAllLog 待导出的数据
* @param response /
* @throws IOException /
*/
void downloadLog(List<QuartzLog> queryAllLog, HttpServletResponse response) throws IOException;
/**
* 执行子任务
*
* @param tasks /
* @throws InterruptedException /
*/
void executionSubJob(String[] tasks) throws InterruptedException;
}
@@ -0,0 +1,173 @@
package cn.odboy.modules.quartz.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.odboy.exception.BadRequestException;
import cn.odboy.modules.quartz.domain.QuartzJob;
import cn.odboy.modules.quartz.domain.QuartzLog;
import cn.odboy.modules.quartz.domain.dto.QuartzJobQueryCriteria;
import cn.odboy.modules.quartz.mapper.QuartzJobMapper;
import cn.odboy.modules.quartz.mapper.QuartzLogMapper;
import cn.odboy.modules.quartz.service.QuartzJobService;
import cn.odboy.modules.quartz.util.QuartzManage;
import cn.odboy.util.*;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.quartz.CronExpression;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
@RequiredArgsConstructor
@Service(value = "quartzJobService")
public class QuartzJobServiceImpl extends ServiceImpl<QuartzJobMapper, QuartzJob> implements QuartzJobService {
private final QuartzJobMapper quartzJobMapper;
private final QuartzLogMapper quartzLogMapper;
private final QuartzManage quartzManage;
private final RedisUtil redisUtil;
@Override
public PageResult<QuartzJob> queryAll(QuartzJobQueryCriteria criteria, Page<Object> page) {
return PageUtil.toPage(quartzJobMapper.findAll(criteria, page));
}
@Override
public PageResult<QuartzLog> queryAllLog(QuartzJobQueryCriteria criteria, Page<Object> page) {
return PageUtil.toPage(quartzLogMapper.findAll(criteria, page));
}
@Override
public List<QuartzJob> queryAll(QuartzJobQueryCriteria criteria) {
return quartzJobMapper.findAll(criteria);
}
@Override
public List<QuartzLog> queryAllLog(QuartzJobQueryCriteria criteria) {
return quartzLogMapper.findAll(criteria);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void create(QuartzJob resources) {
if (!CronExpression.isValidExpression(resources.getCronExpression())) {
throw new BadRequestException("cron表达式格式错误");
}
save(resources);
quartzManage.addJob(resources);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(QuartzJob resources) {
if (!CronExpression.isValidExpression(resources.getCronExpression())) {
throw new BadRequestException("cron表达式格式错误");
}
if (StringUtil.isNotBlank(resources.getSubTask())) {
List<String> tasks = Arrays.asList(resources.getSubTask().split("[,,]"));
if (tasks.contains(resources.getId().toString())) {
throw new BadRequestException("子任务中不能添加当前任务ID");
}
}
saveOrUpdate(resources);
quartzManage.updateJobCron(resources);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateIsPause(QuartzJob quartzJob) {
// 置换暂停状态
if (quartzJob.getIsPause()) {
quartzManage.resumeJob(quartzJob);
quartzJob.setIsPause(false);
} else {
quartzManage.pauseJob(quartzJob);
quartzJob.setIsPause(true);
}
saveOrUpdate(quartzJob);
}
@Override
public void execution(QuartzJob quartzJob) {
quartzManage.runJobNow(quartzJob);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Set<Long> ids) {
for (Long id : ids) {
QuartzJob quartzJob = getById(id);
quartzManage.deleteJob(quartzJob);
removeById(quartzJob);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void executionSubJob(String[] tasks) throws InterruptedException {
for (String id : tasks) {
if (StrUtil.isBlank(id)) {
// 如果是手动清除子任务id,会出现id为空字符串的问题
continue;
}
QuartzJob quartzJob = getById(Long.parseLong(id));
// 执行任务
String uuid = IdUtil.simpleUUID();
quartzJob.setUuid(uuid);
// 执行任务
execution(quartzJob);
// 获取执行状态,如果执行失败则停止后面的子任务执行
Boolean result = redisUtil.get(uuid, Boolean.class);
while (result == null) {
// 休眠5秒,再次获取子任务执行情况
Thread.sleep(5000);
result = redisUtil.get(uuid, Boolean.class);
}
if (!result) {
redisUtil.del(uuid);
break;
}
}
}
@Override
public void download(List<QuartzJob> quartzJobs, HttpServletResponse response) throws IOException {
List<Map<String, Object>> list = new ArrayList<>();
for (QuartzJob quartzJob : quartzJobs) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("任务名称", quartzJob.getJobName());
map.put("Bean名称", quartzJob.getBeanName());
map.put("执行方法", quartzJob.getMethodName());
map.put("参数", quartzJob.getParams());
map.put("表达式", quartzJob.getCronExpression());
map.put("状态", quartzJob.getIsPause() ? "暂停中" : "运行中");
map.put("描述", quartzJob.getDescription());
map.put("创建日期", quartzJob.getCreateTime());
list.add(map);
}
FileUtil.downloadExcel(list, response);
}
@Override
public void downloadLog(List<QuartzLog> queryAllLog, HttpServletResponse response) throws IOException {
List<Map<String, Object>> list = new ArrayList<>();
for (QuartzLog quartzLog : queryAllLog) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("任务名称", quartzLog.getJobName());
map.put("Bean名称", quartzLog.getBeanName());
map.put("执行方法", quartzLog.getMethodName());
map.put("参数", quartzLog.getParams());
map.put("表达式", quartzLog.getCronExpression());
map.put("异常详情", quartzLog.getExceptionDetail());
map.put("耗时/毫秒", quartzLog.getTime());
map.put("状态", quartzLog.getIsSuccess() ? "成功" : "失败");
map.put("创建日期", quartzLog.getCreateTime());
list.add(map);
}
FileUtil.downloadExcel(list, response);
}
}
@@ -0,0 +1,21 @@
package cn.odboy.modules.quartz.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class TestTask {
public void run() {
log.info("run 执行成功");
}
public void run1(String str) {
log.info("run1 执行成功,参数为: {}", str);
}
public void run2() {
log.info("run2 执行成功");
}
}
@@ -0,0 +1,115 @@
package cn.odboy.modules.quartz.util;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import cn.odboy.domain.dto.EmailDto;
import cn.odboy.modules.quartz.domain.QuartzJob;
import cn.odboy.modules.quartz.domain.QuartzLog;
import cn.odboy.modules.quartz.mapper.QuartzLogMapper;
import cn.odboy.modules.quartz.service.QuartzJobService;
import cn.odboy.service.EmailService;
import cn.odboy.util.RedisUtil;
import cn.odboy.util.SpringBeanHolder;
import cn.odboy.util.StringUtil;
import cn.odboy.util.ThrowableUtil;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
public class ExecutionJob extends QuartzJobBean {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
// 此处仅供参考,可根据任务执行情况自定义线程池参数
private final ThreadPoolTaskExecutor executor = SpringBeanHolder.getBean("taskAsync");
@Override
public void executeInternal(JobExecutionContext context) {
// 获取任务
QuartzJob quartzJob = (QuartzJob) context.getMergedJobDataMap().get(QuartzJob.JOB_KEY);
// 获取spring bean
QuartzLogMapper quartzLogMapper = SpringBeanHolder.getBean(QuartzLogMapper.class);
QuartzJobService quartzJobService = SpringBeanHolder.getBean(QuartzJobService.class);
RedisUtil redisUtil = SpringBeanHolder.getBean(RedisUtil.class);
String uuid = quartzJob.getUuid();
QuartzLog log = new QuartzLog();
log.setJobName(quartzJob.getJobName());
log.setBeanName(quartzJob.getBeanName());
log.setMethodName(quartzJob.getMethodName());
log.setParams(quartzJob.getParams());
long startTime = System.currentTimeMillis();
log.setCronExpression(quartzJob.getCronExpression());
try {
// 执行任务
QuartzRunnable task = new QuartzRunnable(quartzJob.getBeanName(), quartzJob.getMethodName(), quartzJob.getParams());
Future<?> future = executor.submit(task);
future.get();
long times = System.currentTimeMillis() - startTime;
log.setTime(times);
if (StringUtil.isNotBlank(uuid)) {
redisUtil.set(uuid, true);
}
// 任务状态
log.setIsSuccess(true);
logger.info("任务执行成功,任务名称:{}, 执行时间:{}毫秒", quartzJob.getJobName(), times);
// 判断是否存在子任务
if (StringUtil.isNotBlank(quartzJob.getSubTask())) {
String[] tasks = quartzJob.getSubTask().split("[,,]");
// 执行子任务
quartzJobService.executionSubJob(tasks);
}
} catch (Exception e) {
if (StringUtil.isNotBlank(uuid)) {
redisUtil.set(uuid, false);
}
logger.error("任务执行失败,任务名称:{}", quartzJob.getJobName());
long times = System.currentTimeMillis() - startTime;
log.setTime(times);
// 任务状态 0:成功 1:失败
log.setIsSuccess(false);
log.setExceptionDetail(ThrowableUtil.getStackTrace(e));
// 任务如果失败了则暂停
if (quartzJob.getPauseAfterFailure() != null && quartzJob.getPauseAfterFailure()) {
quartzJob.setIsPause(false);
//更新状态
quartzJobService.updateIsPause(quartzJob);
}
if (quartzJob.getEmail() != null) {
EmailService emailService = SpringBeanHolder.getBean(EmailService.class);
// 邮箱报警
if (StringUtil.isNoneBlank(quartzJob.getEmail())) {
EmailDto emailDto = taskAlarm(quartzJob, ThrowableUtil.getStackTrace(e));
emailService.send(emailDto, emailService.find());
}
}
} finally {
quartzLogMapper.insert(log);
}
}
private EmailDto taskAlarm(QuartzJob quartzJob, String msg) {
EmailDto emailDto = new EmailDto();
emailDto.setSubject("定时任务【" + quartzJob.getJobName() + "】执行失败,请尽快处理!");
Map<String, Object> data = new HashMap<>(16);
data.put("task", quartzJob);
data.put("msg", msg);
TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH));
Template template = engine.getTemplate("taskAlarm.ftl");
emailDto.setContent(template.render(data));
List<String> emails = Arrays.asList(quartzJob.getEmail().split("[,,]"));
emailDto.setTos(emails);
return emailDto;
}
}
@@ -0,0 +1,165 @@
package cn.odboy.modules.quartz.util;
import cn.odboy.exception.BadRequestException;
import cn.odboy.modules.quartz.domain.QuartzJob;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import static org.quartz.TriggerBuilder.newTrigger;
@Slf4j
@Component
public class QuartzManage {
private static final String JOB_NAME = "TASK_";
@Resource
private Scheduler scheduler;
public void addJob(QuartzJob quartzJob) {
try {
// 构建job信息
JobDetail jobDetail = JobBuilder.newJob(ExecutionJob.class).
withIdentity(JOB_NAME + quartzJob.getId()).build();
//通过触发器名和cron 表达式创建 Trigger
Trigger cronTrigger = newTrigger()
.withIdentity(JOB_NAME + quartzJob.getId())
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(quartzJob.getCronExpression()))
.build();
cronTrigger.getJobDataMap().put(QuartzJob.JOB_KEY, quartzJob);
//重置启动时间
((CronTriggerImpl) cronTrigger).setStartTime(new Date());
//执行定时任务,如果是持久化的,这里会报错,捕获输出
try {
scheduler.scheduleJob(jobDetail, cronTrigger);
} catch (ObjectAlreadyExistsException e) {
log.warn("定时任务已存在,跳过加载");
}
// 暂停任务
if (quartzJob.getIsPause()) {
pauseJob(quartzJob);
}
} catch (Exception e) {
log.error("创建定时任务失败", e);
throw new BadRequestException("创建定时任务失败");
}
}
/**
* 更新job cron表达式
*
* @param quartzJob /
*/
public void updateJobCron(QuartzJob quartzJob) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId());
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 如果不存在则创建一个定时任务
if (trigger == null) {
addJob(quartzJob);
trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
}
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzJob.getCronExpression());
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
//重置启动时间
((CronTriggerImpl) trigger).setStartTime(new Date());
trigger.getJobDataMap().put(QuartzJob.JOB_KEY, quartzJob);
scheduler.rescheduleJob(triggerKey, trigger);
// 暂停任务
if (quartzJob.getIsPause()) {
pauseJob(quartzJob);
}
} catch (Exception e) {
log.error("更新定时任务失败", e);
throw new BadRequestException("更新定时任务失败");
}
}
/**
* 删除一个job
*
* @param quartzJob /
*/
public void deleteJob(QuartzJob quartzJob) {
try {
JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId());
scheduler.pauseJob(jobKey);
scheduler.deleteJob(jobKey);
} catch (Exception e) {
log.error("删除定时任务失败", e);
throw new BadRequestException("删除定时任务失败");
}
}
/**
* 恢复一个job
*
* @param quartzJob /
*/
public void resumeJob(QuartzJob quartzJob) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId());
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 如果不存在则创建一个定时任务
if (trigger == null) {
addJob(quartzJob);
}
JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId());
scheduler.resumeJob(jobKey);
} catch (Exception e) {
log.error("恢复定时任务失败", e);
throw new BadRequestException("恢复定时任务失败");
}
}
/**
* 立即执行job
*
* @param quartzJob /
*/
public void runJobNow(QuartzJob quartzJob) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId());
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 如果不存在则创建一个定时任务
if (trigger == null) {
addJob(quartzJob);
}
JobDataMap dataMap = new JobDataMap();
dataMap.put(QuartzJob.JOB_KEY, quartzJob);
JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId());
scheduler.triggerJob(jobKey, dataMap);
} catch (Exception e) {
log.error("定时任务执行失败", e);
throw new BadRequestException("定时任务执行失败");
}
}
/**
* 暂停一个job
*
* @param quartzJob /
*/
public void pauseJob(QuartzJob quartzJob) {
try {
JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId());
scheduler.pauseJob(jobKey);
} catch (Exception e) {
log.error("定时任务暂停失败", e);
throw new BadRequestException("定时任务暂停失败");
}
}
}
@@ -0,0 +1,43 @@
package cn.odboy.modules.quartz.util;
import cn.odboy.util.SpringBeanHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
/**
* 执行定时任务
*/
@Slf4j
public class QuartzRunnable implements Callable<Object> {
private final Object target;
private final Method method;
private final String params;
QuartzRunnable(String beanName, String methodName, String params)
throws NoSuchMethodException, SecurityException {
this.target = SpringBeanHolder.getBean(beanName);
this.params = params;
if (StringUtils.isNotBlank(params)) {
this.method = target.getClass().getDeclaredMethod(methodName, String.class);
} else {
this.method = target.getClass().getDeclaredMethod(methodName);
}
}
@Override
@SuppressWarnings({"unchecked", "all"})
public Object call() throws Exception {
ReflectionUtils.makeAccessible(method);
if (StringUtils.isNotBlank(params)) {
method.invoke(target, params);
} else {
method.invoke(target);
}
return null;
}
}
@@ -0,0 +1,120 @@
package cn.odboy.modules.security.config;
import cn.odboy.exception.BadRequestException;
import cn.odboy.modules.security.config.enums.LoginCodeEnum;
import cn.odboy.util.StringUtil;
import com.wf.captcha.*;
import com.wf.captcha.base.Captcha;
import lombok.Data;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.awt.*;
/**
* 登录验证码配置信息
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "login.code")
public class CaptchaConfig {
/**
* 验证码配置
*/
@Getter
private LoginCodeEnum codeType;
/**
* 验证码有效期 分钟
*/
private Long expiration = 5L;
/**
* 验证码内容长度
*/
private int length = 4;
/**
* 验证码宽度
*/
private int width = 111;
/**
* 验证码高度
*/
private int height = 36;
/**
* 验证码字体
*/
private String fontName;
/**
* 字体大小
*/
private int fontSize = 25;
/**
* 依据配置信息生产验证码
*
* @return /
*/
public Captcha getCaptcha() {
Captcha captcha;
switch (codeType) {
case ARITHMETIC:
// 算术类型 https://gitee.com/whvse/EasyCaptcha
captcha = new FixedArithmeticCaptcha(width, height);
// 几位数运算,默认是两位
captcha.setLen(length);
break;
case CHINESE:
captcha = new ChineseCaptcha(width, height);
captcha.setLen(length);
break;
case CHINESE_GIF:
captcha = new ChineseGifCaptcha(width, height);
captcha.setLen(length);
break;
case GIF:
captcha = new GifCaptcha(width, height);
captcha.setLen(length);
break;
case SPEC:
captcha = new SpecCaptcha(width, height);
captcha.setLen(length);
break;
default:
throw new BadRequestException("验证码配置信息错误!正确配置查看 LoginCodeEnum ");
}
if (StringUtil.isNotBlank(fontName)) {
captcha.setFont(new Font(fontName, Font.PLAIN, fontSize));
}
return captcha;
}
static class FixedArithmeticCaptcha extends ArithmeticCaptcha {
public FixedArithmeticCaptcha(int width, int height) {
super(width, height);
}
@Override
protected char[] alphas() {
// 生成随机数字和运算符
int n1 = num(1, 10), n2 = num(1, 10);
int opt = num(3);
// 计算结果
int res = new int[]{n1 + n2, n1 - n2, n1 * n2}[opt];
// 转换为字符运算符
char optChar = "+-x".charAt(opt);
this.setArithmeticString(String.format("%s%c%s=?", n1, optChar, n2));
this.chars = String.valueOf(res);
return chars.toCharArray();
}
}
}
@@ -0,0 +1,21 @@
package cn.odboy.modules.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 登录配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "login")
public class LoginProperties {
/**
* 账号单用户 登录
*/
private boolean singleLogin = false;
public static final String cacheKey = "user_login_cache:";
}
@@ -0,0 +1,58 @@
package cn.odboy.modules.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* Jwt参数配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
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,119 @@
package cn.odboy.modules.security.config;
import cn.odboy.constant.RequestMethodEnum;
import cn.odboy.modules.security.security.JwtAccessDeniedHandler;
import cn.odboy.modules.security.security.JwtAuthenticationEntryPoint;
import cn.odboy.modules.security.security.TokenConfigurer;
import cn.odboy.modules.security.security.TokenProvider;
import cn.odboy.modules.security.service.OnlineUserService;
import cn.odboy.util.AnonTagUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.filter.CorsFilter;
import java.util.Map;
import java.util.Set;
@Configuration
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SpringSecurityConfig {
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 OnlineUserService onlineUserService;
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
// 去除 ROLE_ 前缀
return new GrantedAuthorityDefaults("");
}
@Bean
public PasswordEncoder passwordEncoder() {
// 密码加密方式
return new BCryptPasswordEncoder();
}
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// 获取匿名标记
Map<String, Set<String>> anonymousUrls = AnonTagUtil.getAnonymousUrl(applicationContext);
return httpSecurity
// 禁用 CSRF
.csrf().disable()
.addFilter(corsFilter)
// 授权异常
.exceptionHandling()
.authenticationEntryPoint(authenticationErrorHandler)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 防止iframe 造成跨域
.and()
.headers()
.frameOptions()
.disable()
// 不创建会话
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 静态资源等等
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/webSocket/**"
).permitAll()
// swagger 文档
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
// 文件
.antMatchers("/avatar/**").permitAll()
.antMatchers("/file/**").permitAll()
// 阿里巴巴 druid
.antMatchers("/druid/**").permitAll()
// 放行OPTIONS请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 自定义匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型
// GET
.antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
// POST
.antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
// PUT
.antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
// PATCH
.antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
// DELETE
.antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
// 所有类型的接口都放行
.antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()
// 所有请求都需要认证
.anyRequest().authenticated()
.and().apply(securityConfigurerAdapter())
.and().build();
}
private TokenConfigurer securityConfigurerAdapter() {
return new TokenConfigurer(tokenProvider, properties, onlineUserService);
}
}

Some files were not shown because too many files have changed in this diff Show More