diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5ae312 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +assets/ +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ +.DS_Store +lib +plugin \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..70fc418 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# 使用 GraalVM 17 作为基础镜像 +FROM ghcr.io/graalvm/graalvm-ce:latest + +# 设置容器内的工作目录 +WORKDIR /app + +# 将可执行的jar文件复制到容器内 +COPY target/cool-admin-7.1.0.jar /app/cool-admin-7.1.0.jar + +# 暴露Spring Boot应用程序运行的端口 +EXPOSE 8001 + +# 运行Spring Boot应用程序的命令 +ENTRYPOINT ["java", "-jar", "/app/cool-admin-7.1.0.jar", "--spring.profiles.active=prod"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5a6831d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 cool-team-official + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c3d54d7..e5c609a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,92 @@ -# cool-admin-java -🔥 cool-admin(java版)一个很酷的后台权限管理框架,Ai编码、流程编排、模块化、插件化、CRUD极速开发,永久开源免费,基于springboot3、typescript、vue3、vite、element-ui等构建 + +

+ Midway Logo +

+

cool-admin(java版)后台权限管理系统,开源免费,Ai编码、流程编排、模块化、插件化,用于快速构建后台应用程序,详情可到官网 进一步了解。 +

+ GitHub license + GitHub tag + GitHub tag +

+ +## 技术栈 + +- 后端:**`Springboot3` `Mybatis-Flex`** +- 前端:**`Vue3` `Vite` `Element-Ui` `Typescript`** +- 数据库:**`Mysql` `Postgresql(适配中)` `Sqlite(适配中)` `...`** + +## 特性 + +Ai时代,很多老旧的框架已经无法满足现代化的开发需求,Cool-Admin开发了一系列的功能,让开发变得更简单、更快速、更高效。 + +- **Ai编码**:通过微调大模型学习框架特有写法,实现简单功能从Api接口到前端页面的一键生成 +- **流程编排**:通过拖拽编排方式,即可实现类似像智能客服这样的功能 +- **模块化**:代码是模块化的,清晰明了,方便维护 +- **插件化**:插件化的设计,可以通过安装插件的方式扩展如:支付、短信、邮件等功能 + +![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/flow.png) + +## 地址 + +- 官网:[https://cool-admin.com](https://cool-admin.com) +- 文档:[https://java.cool-admin.com](https://java.cool-admin.com) + +## 演示 + +[https://show.cool-admin.com](https://show.cool-admin.com) + +- 账户:admin +- 密码:123456 + +![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/home-mini.png) + +#### 项目前端 + +系统是前后端分离的,启动完成后,还需要启动前端项目,前端项目地址: + +[https://github.com/cool-team-official/cool-admin-vue](https://github.com/cool-team-official/cool-admin-vue) + +或 + +[https://gitee.com/cool-team-official/cool-admin-vue](https://gitee.com/cool-team-official/cool-admin-vue) + +## 微信群 + +Admin Wechat + +## 运行 + +### 环境要求 + +- Java Graalvm 17+ +- Maven 3.6+ + +### 配置 + +修改数据库配置,配置文件位于`src/resources/application-local.yml` + +以 Mysql 为例,其他数据库请参考[数据库配置文档](https://cool-js.com/admin/node/quick.html#%E6%95%B0%E6%8D%AE%E5%BA%93%E9%85%8D%E7%BD%AE) + +Mysql(`>=5.7版本`),建议 8.0,首次启动会自动初始化并导入数据 + +```yaml +# mysql,驱动已经内置,无需安装 +spring: + datasource: + url: jdbc:mysql://127.0.0.1:3306/cool?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8 + username: root + password: 123456 + driver-class-name: com.mysql.cj.jdbc.Driver +``` + +### 启动 + +注:项目使用到了[Mybatis-Flex 的Apt功能](https://mybatis-flex.com/zh/others/apt.html),如果启动报错,请先执行`mvn compile`编译 + +1、启动文件:`src/main/java/com/cool/CoolApplication.java` + +2、启动完成后,访问:[http://localhost:8001](http://localhost:8001) + +3、如果看到以下界面,说明启动成功。这时候再启动前端项目即可,数据库会自动初始化,默认账号:admin,密码:123456 + +![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/run.png) \ No newline at end of file diff --git a/cool-admin.iml b/cool-admin.iml new file mode 100644 index 0000000..738cf87 --- /dev/null +++ b/cool-admin.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a296994 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +# 本地数据库环境 +# 数据存放在当前目录下的 data里 +# 推荐使用安装了docker扩展的vscode打开目录 在本文件上右键可以快速启动,停止 +# 如不需要相关容器开机自启动,可注释掉 restart: always +# 如遇端口冲突 可调整ports下 :前面的端口号 +version: "3.1" + +services: + coolDB: + image: mysql + command: + --default-authentication-plugin=mysql_native_password + --sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION + --group_concat_max_len=102400 + restart: always + volumes: + - ./data/mysql/:/var/lib/mysql/ + environment: + TZ: Asia/Shanghai # 指定时区 + MYSQL_ROOT_PASSWORD: "123456" # 配置root用户密码 + MYSQL_DATABASE: "cool" # 业务库名 + MYSQL_USER: "root" # 业务库用户名 + MYSQL_PASSWORD: "123456" # 业务库密码 + networks: + - cool + ports: + - 3306:3306 + + coolRedis: + image: redis + #command: --requirepass "12345678" # redis库密码,不需要密码注释本行 + restart: always + environment: + TZ: Asia/Shanghai # 指定时区 + volumes: + - ./data/redis/:/data/ + networks: + - cool + ports: + - 6379:6379 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f480991 --- /dev/null +++ b/pom.xml @@ -0,0 +1,189 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.cool + cool-admin + 7.1.0 + cool-admin + cool admin for java + + 17 + 1.18.34 + 1.9.3 + 1.9.3.021-beta + 5.8.26 + 3.3.2 + 2.0.51 + 2.5.0 + + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-quartz + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-configuration-processor + + + org.springframework.boot + spring-boot-starter-aop + + + + com.github.ben-manes.caffeine + caffeine + + + com.mysql + mysql-connector-j + runtime + + + com.zaxxer + HikariCP + + + org.projectlombok + lombok + true + + + cn.hutool + hutool-all + ${hutool.version} + + + ognl + ognl + ${ognl.version} + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + + + com.tangzc + mybatis-flex-ext-spring-boot3-starter + ${mybatis-flex.ext.version} + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc-openapi.version} + + + jakarta.servlet + jakarta.servlet-api + provided + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + com.mybatis-flex + mybatis-flex-processor + ${mybatis-flex.version} + + + + + + + + + + local + + true + + + local + + + + prod + + prod + + + + + \ No newline at end of file diff --git a/src/main/java/com/cool/CoolApplication.java b/src/main/java/com/cool/CoolApplication.java new file mode 100644 index 0000000..87be694 --- /dev/null +++ b/src/main/java/com/cool/CoolApplication.java @@ -0,0 +1,25 @@ +package com.cool; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.scheduling.annotation.EnableAsync; + +import com.tangzc.autotable.springboot.EnableAutoTable; + +/** + * CoolApplication - 应用程序的主类 + * 该类配置并运行应用程序。 + */ +@EnableAutoTable // 开启自动建表 +@EnableAsync // 开启异步处理 +@EnableCaching // 开启缓存 +@SpringBootApplication +@MapperScan("com.cool.modules.*.mapper") // 扫描指定包中的MyBatis映射器 +public class CoolApplication { + + public static void main(String[] args) { + SpringApplication.run(CoolApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/annotation/CoolPlugin.java b/src/main/java/com/cool/core/annotation/CoolPlugin.java new file mode 100644 index 0000000..a010335 --- /dev/null +++ b/src/main/java/com/cool/core/annotation/CoolPlugin.java @@ -0,0 +1,12 @@ +package com.cool.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CoolPlugin { + String value() default ""; +} diff --git a/src/main/java/com/cool/core/annotation/CoolRestController.java b/src/main/java/com/cool/core/annotation/CoolRestController.java new file mode 100644 index 0000000..6fd5b4c --- /dev/null +++ b/src/main/java/com/cool/core/annotation/CoolRestController.java @@ -0,0 +1,26 @@ +package com.cool.core.annotation; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.annotation.*; + +/** + * 自定义路由注解 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RestController +@RequestMapping +public @interface CoolRestController { + + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + String[] api() default {}; +} diff --git a/src/main/java/com/cool/core/annotation/IgnoreRecycleData.java b/src/main/java/com/cool/core/annotation/IgnoreRecycleData.java new file mode 100644 index 0000000..6ceb893 --- /dev/null +++ b/src/main/java/com/cool/core/annotation/IgnoreRecycleData.java @@ -0,0 +1,12 @@ +package com.cool.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface IgnoreRecycleData { + String value() default ""; +} diff --git a/src/main/java/com/cool/core/base/BaseController.java b/src/main/java/com/cool/core/base/BaseController.java new file mode 100644 index 0000000..3a19744 --- /dev/null +++ b/src/main/java/com/cool/core/base/BaseController.java @@ -0,0 +1,226 @@ +package com.cool.core.base; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.TypeUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.request.CrudOption; +import com.cool.core.request.R; +import com.cool.core.util.SpringContextUtils; +import com.mybatisflex.core.paginate.Page; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 控制层基类 + * + * @param + * @param + */ +public abstract class BaseController, T extends BaseEntity> { + + @Getter + protected S service; + protected Class entityClass; + + @PostConstruct + private void init() { + if (entityClass == null) { + this.entityClass = currentEntityClass(); + } + if (service == null) { + this.service = (S) SpringContextUtils.getBean(currentServiceClass()); + } + } + + private final String COOL_PAGE_OP = "COOL_PAGE_OP"; + private final String COOL_LIST_OP = "COOL_LIST_OP"; + + private final ThreadLocal> pageOption = new ThreadLocal<>(); + private final ThreadLocal> listOption = new ThreadLocal<>(); + private final ThreadLocal requestParams = new ThreadLocal<>(); + + @ModelAttribute + protected void preHandle(HttpServletRequest request, + @RequestAttribute JSONObject requestParams) { + this.pageOption.set(new CrudOption<>(requestParams)); + this.listOption.set(new CrudOption<>(requestParams)); + this.requestParams.set(requestParams); + init(request, requestParams); + request.setAttribute(COOL_PAGE_OP, this.pageOption.get()); + request.setAttribute(COOL_LIST_OP, this.listOption.get()); + removeThreadLocal(); + } + + /** + * 手动移除变量 + */ + private void removeThreadLocal() { + this.listOption.remove(); + this.pageOption.remove(); + this.requestParams.remove(); + } + + public CrudOption createOp() { + return new CrudOption<>(this.requestParams.get()); + } + + public void setListOption(CrudOption listOption) { + this.listOption.set(listOption); + } + + public void setPageOption(CrudOption pageOption) { + this.pageOption.set(pageOption); + } + + protected abstract void init(HttpServletRequest request, JSONObject requestParams); + + /** + * 新增 + *

+ * // * @param t 实体对象 + */ + @Operation(summary = "新增", description = "新增信息,对应后端的实体类") + @PostMapping("/add") + protected R add(@RequestAttribute() JSONObject requestParams) { + String body = requestParams.getStr("body"); + if (JSONUtil.isTypeJSONArray(body)) { + JSONArray array = JSONUtil.parseArray(body); + return R.ok(Dict.create() + .set("ids", service.addBatch(requestParams, array.toList(currentEntityClass())))); + } else { + return R.ok(Dict.create().set("id", + service.add(requestParams, JSONUtil.parseObj(body).toBean(currentEntityClass())))); + } + } + + /** + * 删除 + * + * @param params 请求参数 ids 数组 或者按","隔开 + */ + @Operation(summary = "删除", description = "支持批量删除 请求参数 ids 数组 或者按\",\"隔开") + @PostMapping("/delete") + protected R delete(HttpServletRequest request, @RequestBody Map params, + @RequestAttribute() JSONObject requestParams) { + service.delete(requestParams, Convert.toLongArray(getIds(params))); + return R.ok(); + } + + /** + * 修改 + * + * @param t 修改对象 + */ + @Operation(summary = "修改", description = "根据ID修改") + @PostMapping("/update") + protected R update(@RequestBody T t, @RequestAttribute() JSONObject requestParams) { + Long id = t.getId(); + JSONObject info = JSONUtil.parseObj(JSONUtil.toJsonStr(service.info(id))); + requestParams.forEach(info::set); + info.set("updateTime", new Date()); + service.update(requestParams, JSONUtil.toBean(info, currentEntityClass())); + return R.ok(); + } + + /** + * 信息 + * + * @param id ID + */ + @Operation(summary = "信息", description = "根据ID查询单个信息") + @GetMapping("/info") + protected R info(@RequestAttribute() JSONObject requestParams, + @RequestParam() Long id) { + return R.ok(service.info(requestParams, id)); + } + + /** + * 列表查询 + * + * @param requestParams 请求参数 + */ + @Operation(summary = "查询", description = "查询多个信息") + @PostMapping("/list") + protected R list(@RequestAttribute() JSONObject requestParams, + @RequestAttribute(COOL_LIST_OP) CrudOption option) { + return R.ok(service.list(requestParams, option.getQueryWrapper(entityClass))); + } + + /** + * 分页查询 + * + * @param requestParams 请求参数 + */ + @Operation(summary = "分页", description = "分页查询多个信息") + @PostMapping("/page") + protected R page(@RequestAttribute() JSONObject requestParams, + @RequestAttribute(COOL_PAGE_OP) CrudOption option) { + Integer page = requestParams.getInt("page", 1); + Integer size = requestParams.getInt("size", 20); + return R.ok( + pageResult((Page) service.page(requestParams, new Page<>(page, size), + option.getQueryWrapper(entityClass)))); + } + + /** + * 分页结果 + * + * @param page 分页返回数据 + */ + protected Map pageResult(Page page) { + Map result = new HashMap<>(); + Map pagination = new HashMap<>(); + pagination.put("size", page.getPageSize()); + pagination.put("page", page.getPageNumber()); + pagination.put("total", page.getTotalRow()); + result.put("list", page.getRecords()); + result.put("pagination", pagination); + return result; + } + + public Class currentEntityClass() { + // 使用 获取泛型参数类型 + Type type = TypeUtil.getTypeArgument(this.getClass(), 1); // 获取第二个泛型参数 + if (type instanceof Class) { + return (Class) type; + } + throw new IllegalStateException("Unable to determine entity class type"); + } + + public Class currentServiceClass() { + // 使用 获取泛型参数类型 + Type type = TypeUtil.getTypeArgument(this.getClass(), 0); // 获取第一个泛型参数 + if (type instanceof Class) { + return (Class) type; + } + throw new IllegalStateException("Unable to determine entity class type"); + } + + protected List getIds(Map params) { + Object ids = params.get("ids"); + CoolPreconditions.checkEmpty(ids, "ids 参数错误"); + if (!(ids instanceof ArrayList)) { + ids = ids.toString().split(","); + } + return Convert.toList(Long.class, ids); + } + +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/base/BaseEntity.java b/src/main/java/com/cool/core/base/BaseEntity.java new file mode 100644 index 0000000..978b74e --- /dev/null +++ b/src/main/java/com/cool/core/base/BaseEntity.java @@ -0,0 +1,36 @@ +package com.cool.core.base; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; +import com.mybatisflex.core.activerecord.Model; +import com.mybatisflex.core.query.QueryWrapper; +import com.tangzc.autotable.annotation.Ignore; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import java.io.Serializable; +import java.util.Date; +import lombok.Getter; +import lombok.Setter; + +/** + * 基础实体类 + */ +@Getter +@Setter +public abstract class BaseEntity> extends Model implements Serializable { + + @Id(keyType = KeyType.Auto, comment = "ID") + private Long id; + + @Column(onInsertValue = "now()") + @ColumnDefine(comment = "创建时间") + private Date createTime; + + @Column(onInsertValue = "now()", onUpdateValue = "now()") + @ColumnDefine(comment = "更新时间") + private Date updateTime; + + @Ignore + @Column(ignore = true) + private QueryWrapper queryWrapper; +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/base/BaseService.java b/src/main/java/com/cool/core/base/BaseService.java new file mode 100644 index 0000000..3f3c882 --- /dev/null +++ b/src/main/java/com/cool/core/base/BaseService.java @@ -0,0 +1,121 @@ +package com.cool.core.base; + +import cn.hutool.json.JSONObject; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.service.IService; + +import java.util.List; + +/** + * 基础service类 + * + * @param 实体 + */ +public interface BaseService extends IService { + /** + * 新增 + * + * @param entity 对应的实体 + */ + Long add(T entity); + + /** + * 新增 + * + * @param requestParams 请求参数 + * @param entity 对应的实体 + * @return ID + */ + Object add(JSONObject requestParams, T entity); + + /** + * 批量添加 + * + * @param requestParams 请求参数 + * @param entitys 请求参数 + * @return ID 集合 + */ + Object addBatch(JSONObject requestParams, List entitys); + + /** + * 删除, 支持单个或者批量删除 + * + * @param ids ID数组 + */ + boolean delete(Long... ids); + + /** + * 多个删除,带请求参数 + * + * @param requestParams 请求参数 + * @param ids ID数组 + */ + boolean delete(JSONObject requestParams, Long... ids); + + /** + * 更新 + * + * @param entity 实体 + */ + boolean update(T entity); + + /** + * 更新 + * + * @param requestParams 请求参数 + * @param entity 实体 + */ + boolean update(JSONObject requestParams, T entity); + + /** + * 查询所有 + * + * @param requestParams 请求参数 + * @param queryWrapper 查询条件 + * @return 列表信息 + */ + Object list(JSONObject requestParams, QueryWrapper queryWrapper); + + /** + * 分页查询 + * + * @param requestParams 请求参数 + * @param page 分页信息 + * @param queryWrapper 查询条件 + * @return 分页信息 + */ + Object page(JSONObject requestParams, Page page, QueryWrapper queryWrapper); + + /** + * 查询信息 + * + * @param id ID + */ + Object info(Long id); + + /** + * 查询信息 + * + * @param requestParams 请求参数 + * @param id ID + */ + Object info(JSONObject requestParams, Long id); + + /** + * 修改之后 + * + * @param requestParams 请求参数 + * @param t 对应实体 + */ + void modifyAfter(JSONObject requestParams, T t); + + /** + * 修改之后 + * + * @param requestParams 请求参数 + * @param t 对应实体 + * @param type 修改类型 + */ + void modifyAfter(JSONObject requestParams, T t, ModifyEnum type); +} diff --git a/src/main/java/com/cool/core/base/BaseServiceImpl.java b/src/main/java/com/cool/core/base/BaseServiceImpl.java new file mode 100644 index 0000000..8ab1d69 --- /dev/null +++ b/src/main/java/com/cool/core/base/BaseServiceImpl.java @@ -0,0 +1,101 @@ +package com.cool.core.base; + +import cn.hutool.json.JSONObject; +import com.mybatisflex.core.BaseMapper; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.spring.service.impl.ServiceImpl; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 基础service实现类 + * + * @param Mapper 类 + * @param 实体 + */ +public class BaseServiceImpl, T extends BaseEntity> extends + ServiceImpl + implements BaseService { + + @Override + public Long add(T entity) { + mapper.insert(entity); + return entity.getId(); + } + + @Override + public Object add(JSONObject requestParams, T entity) { + this.add(entity); + this.modifyAfter(requestParams, entity, ModifyEnum.ADD); + return entity.getId(); + } + + @Override + public Object addBatch(JSONObject requestParams, List entitys) { + List ids = new ArrayList<>(); + entitys.forEach(e -> ids.add(this.add(e))); + requestParams.set("ids", ids); + this.modifyAfter(requestParams, null, ModifyEnum.ADD); + return ids; + } + + @Override + public boolean delete(Long... ids) { + return mapper.deleteBatchByIds(Arrays.asList(ids)) > 0; + } + + @Override + public boolean delete(JSONObject requestParams, Long... ids) { + boolean flag = this.delete(ids); + if (flag) { + this.modifyAfter(requestParams, null, ModifyEnum.DELETE); + } + return flag; + } + + @Override + public boolean update(T entity) { + return mapper.update(entity) > 0; + } + + @Override + public boolean update(JSONObject requestParams, T entity) { + boolean flag = this.update(entity); + if (flag) { + this.modifyAfter(requestParams, entity, ModifyEnum.UPDATE); + } + return flag; + } + + @Override + public Object list(JSONObject requestParams, QueryWrapper queryWrapper) { + return this.list(queryWrapper); + } + + @Override + public Object page(JSONObject requestParams, Page page, QueryWrapper queryWrapper) { + return this.page(page, queryWrapper); + } + + @Override + public Object info(JSONObject requestParams, Long id) { + return info(id); + } + + @Override + public Object info(Long id) { + return mapper.selectOneById(id); + } + + @Override + public void modifyAfter(JSONObject requestParams, T t) { + + } + + @Override + public void modifyAfter(JSONObject requestParams, T t, ModifyEnum type) { + modifyAfter(requestParams, t); + } +} diff --git a/src/main/java/com/cool/core/base/ModifyEnum.java b/src/main/java/com/cool/core/base/ModifyEnum.java new file mode 100644 index 0000000..22b0d85 --- /dev/null +++ b/src/main/java/com/cool/core/base/ModifyEnum.java @@ -0,0 +1,13 @@ +package com.cool.core.base; + +/** + * 修改枚举 + */ +public enum ModifyEnum { + // 新增 + ADD, + // 修改 + UPDATE, + // 删除 + DELETE +} diff --git a/src/main/java/com/cool/core/base/controller/CommonController.java b/src/main/java/com/cool/core/base/controller/CommonController.java new file mode 100644 index 0000000..b2b74a4 --- /dev/null +++ b/src/main/java/com/cool/core/base/controller/CommonController.java @@ -0,0 +1,53 @@ +package com.cool.core.base.controller; + +import cn.hutool.core.util.ObjUtil; +import com.cool.core.plugin.service.CoolPluginService; +import com.cool.core.util.ConvertUtil; +import com.cool.core.util.CoolPluginInvokers; +import java.io.File; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; + +@Controller +@RequiredArgsConstructor +public class CommonController { + + final private CoolPluginService coolPluginService; + + @RequestMapping("/") + public String welcome() { + return "welcome"; + } + + @PostMapping("/testPlugin/invokeMethod") + @ResponseBody + public String invokeMethod(@RequestParam String key, @RequestParam String methodName) { + Object result = null; + if (ObjUtil.isEmpty(methodName)) { + result = CoolPluginInvokers.invokePlugin(key); + } else { + result = CoolPluginInvokers.invokePlugin(key, methodName); + } + System.out.println(result); + return "invokeMethod Result: " + result; + } + + /** + * 指定目录加载插件 + */ + @PostMapping("/testPlugin/reload") + @ResponseBody + public String reload() { + // 替换掉自己插件编译的路径,无需在页面上上传 + File file = new File( + "/Users/mac/work/cool_new/cool-admin-java-plugin/target/my_cool_plugin.cool"); + MultipartFile multipartFile = ConvertUtil.convertToMultipartFile(file); + coolPluginService.install(multipartFile, true); + return "reload Success"; + } +} diff --git a/src/main/java/com/cool/core/base/service/MapperProviderService.java b/src/main/java/com/cool/core/base/service/MapperProviderService.java new file mode 100644 index 0000000..8492857 --- /dev/null +++ b/src/main/java/com/cool/core/base/service/MapperProviderService.java @@ -0,0 +1,52 @@ +package com.cool.core.base.service; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.TypeUtil; +import com.cool.core.util.SpringContextUtils; +import com.mybatisflex.core.BaseMapper; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import org.springframework.stereotype.Service; + +@Service +public class MapperProviderService { + + private Map, BaseMapper> mapperMap; + + /** + * 初始化mapperMap,key 为entityClass,value 为 mapper + */ + private void init() { + // 获取所有BaseMapper类型的Bean + Map beansOfType = SpringContextUtils.getBeansOfType(BaseMapper.class); + mapperMap = new HashMap<>(); + for (BaseMapper mapper : beansOfType.values()) { + // 通过反射获取泛型参数,即实体类 + Class entityClass = getGenericType(mapper); + if (entityClass != null) { + mapperMap.put(entityClass, mapper); + } + } + } + + /** + * 通过entity类获取 对应的mapper接口 + */ + public BaseMapper getMapperByEntityClass(Class entityClass) { + if (ObjUtil.isEmpty(mapperMap)) { + init(); + } + return (BaseMapper) mapperMap.get(entityClass); + } + + /** + * 获取mapper对应的entity对象 + */ + private Class getGenericType(BaseMapper mapper) { + // 使用 获取泛型参数类型 + Type[] types = mapper.getClass().getGenericInterfaces(); + Type typeArgument = TypeUtil.getTypeArgument(types[0], 0); + return ObjUtil.isEmpty(typeArgument) ? null : (Class) typeArgument; + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/cache/CoolCache.java b/src/main/java/com/cool/core/cache/CoolCache.java new file mode 100644 index 0000000..fc827a9 --- /dev/null +++ b/src/main/java/com/cool/core/cache/CoolCache.java @@ -0,0 +1,159 @@ +package com.cool.core.cache; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.cool.core.util.ConvertUtil; +import jakarta.annotation.PostConstruct; +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.cache.CacheType; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.stereotype.Component; + +/** + * 缓存工具类 + */ +@EnableCaching +@Configuration +@Component +@RequiredArgsConstructor +public class CoolCache { + + // 缓存类型 + @Value("${spring.cache.type}") + private String type; + + // redis + public RedisCacheWriter redisCache; + + private Cache cache; + + @Value("${cool.cacheName}") + private String cacheName; + + private final static String NULL_VALUE = "@_NULL_VALUE$@"; + + final private CacheManager cacheManager; + + @PostConstruct + private void init() { + cache = cacheManager.getCache(cacheName); + this.type = type.toLowerCase(); + assert cache != null : "Cache not found: " + cacheName; // Ensure cache is not null + if (type.equalsIgnoreCase(CacheType.REDIS.name())) { + redisCache = (RedisCacheWriter) cache.getNativeCache(); + } + } + + /** + * 删除缓存 + * + * @param keys 一个或多个key + */ + public void del(String... keys) { + if (type.equalsIgnoreCase(CacheType.CAFFEINE.name()) || type.equalsIgnoreCase( + CacheType.JCACHE.name())) { + Arrays.stream(keys).forEach(o -> cache.evict(o)); + } + if (type.equalsIgnoreCase(CacheType.REDIS.name())) { + Arrays.stream(keys).forEach(key -> redisCache.remove(cacheName, key.getBytes())); + } + } + + /** + * 普通缓存获取 + * + * @param key 键 + */ + public Object get(String key) { + Object ifNullValue = getIfNullValue(key); + if (ObjUtil.equals(ifNullValue, NULL_VALUE)) { + return null; + } + return ifNullValue; + } + + private Object getIfNullValue(String key) { + if (type.equalsIgnoreCase(CacheType.CAFFEINE.name()) || type.equalsIgnoreCase( + CacheType.JCACHE.name())) { + Cache.ValueWrapper valueWrapper = cache.get(key); + if (valueWrapper != null) { + return valueWrapper.get(); // 获取实际的缓存值 + } + } + if (type.equalsIgnoreCase(CacheType.REDIS.name())) { + byte[] bytes = redisCache.get(cacheName, key.getBytes()); + if (bytes != null && bytes.length > 0) { + return ConvertUtil.toObject(bytes); + } + } + return null; + } + + /** + * 获得对象 + * + * @param key 键 + * @param valueType 值类型 + */ + public T get(String key, Class valueType) { + Object result = get(key); + if (result != null && JSONUtil.isTypeJSONObject(result.toString())) { + return JSONUtil.parseObj(result).toBean(valueType); + } + return result != null ? (T) result : null; + } + + /** + * 获得缓存类型 + */ + public String getMode() { + return this.type; + } + + /** + * 获得原生缓存实例 + */ + public Object getMetaCache() { + return this.cache; + } + + /** + * 普通缓存放入 + * + * @param key 键 + * @param value 值 + */ + public void set(String key, Object value) { + set(key, value, 0); + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param ttl 时间(秒) time要大于0 如果time小于等于0 将设置无限期 + */ + public void set(String key, Object value, long ttl) { + if (ObjUtil.isNull(value)) { + value = NULL_VALUE; + } + if (type.equalsIgnoreCase(CacheType.CAFFEINE.name()) || type.equalsIgnoreCase( + CacheType.JCACHE.name())) { + // 放入缓存 + cache.put(key, value); + } + if (type.equalsIgnoreCase(CacheType.REDIS.name())) { + redisCache.put(cacheName, key.getBytes(), ObjectUtil.serialize(value), + java.time.Duration.ofSeconds(ttl)); + } + } + +} diff --git a/src/main/java/com/cool/core/code/CodeGenerator.java b/src/main/java/com/cool/core/code/CodeGenerator.java new file mode 100644 index 0000000..37a8400 --- /dev/null +++ b/src/main/java/com/cool/core/code/CodeGenerator.java @@ -0,0 +1,108 @@ +package com.cool.core.code; + +import cn.hutool.core.io.file.FileWriter; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.StrUtil; +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 jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Component; + +/** + * 代码生成器 + */ +@Component +public class CodeGenerator { + + private TemplateEngine templateEngine; + + private String baseSrcPath; + + private String baseResPath; + + @PostConstruct + public void init() { + templateEngine = coolTemplateEngine(); + baseSrcPath = System.getProperty("user.dir") + "/src/main/java/com/cool/modules/"; + baseResPath = System.getProperty("user.dir") + "/src/main/resources/"; + } + + public TemplateEngine coolTemplateEngine() { + return TemplateUtil.createEngine( + new TemplateConfig("cool/code", TemplateConfig.ResourceMode.CLASSPATH)); + } + + private String filePath(CodeModel codeModel, String type) { + if (type.equals("controller")) { + return StrUtil.isEmpty(codeModel.getSubModule()) + ? baseSrcPath + codeModel.getModule() + "/" + type + "/" + codeModel.getType() + .value() + : baseSrcPath + codeModel.getModule() + "/" + type + "/" + codeModel.getType() + .value() + "/" + + codeModel.getSubModule(); + } + if (type.equals("xmlMapper")) { + return StrUtil.isEmpty(codeModel.getSubModule()) ? baseResPath + "mapper/" + + codeModel.getModule() + : baseResPath + "mapper/" + codeModel.getModule() + "/" + codeModel.getSubModule(); + } + return StrUtil.isEmpty(codeModel.getSubModule()) ? baseSrcPath + codeModel.getModule() + "/" + + type + : baseSrcPath + codeModel.getModule() + "/" + type + "/" + codeModel.getSubModule(); + } + + /** + * 生成Mapper + * + * @param codeModel 代码模型 + */ + public void mapper(CodeModel codeModel) { + Template template = templateEngine.getTemplate("/mapper/interface.th"); + String result = template.render(Dict.parse(codeModel)); + FileWriter writer = new FileWriter( + filePath(codeModel, "mapper") + "/" + codeModel.getEntity() + "Mapper.java"); + writer.write(result); + } + + /** + * 生成Service + * + * @param codeModel 代码模型 + */ + public void service(CodeModel codeModel) { + Template interfaceTemplate = templateEngine.getTemplate("/service/interface.th"); + String interfaceResult = interfaceTemplate.render(Dict.parse(codeModel)); + FileWriter interfaceWriter = new FileWriter( + filePath(codeModel, "service") + "/" + codeModel.getEntity() + "Service.java"); + interfaceWriter.write(interfaceResult); + + Template template = templateEngine.getTemplate("/service/impl.th"); + String result = template.render(Dict.parse(codeModel)); + FileWriter writer = new FileWriter( + filePath(codeModel, "service") + "/impl/" + codeModel.getEntity() + "ServiceImpl.java"); + writer.write(result); + } + + /** + * 生成Controller + * + * @param codeModel 代码模型 + */ + public void controller(CodeModel codeModel) { + Template template = templateEngine.getTemplate("controller.th"); + System.out.println(codeModel.getType().value()); + Dict data = Dict.create().set("upperType", StrUtil.upperFirst(codeModel.getType().value())) + .set("url", + "/" + codeModel.getType() + "/" + StrUtil.toUnderlineCase(codeModel.getEntity()) + .replace("_", "/")); + data.putAll(Dict.parse(codeModel)); + data.set("type", codeModel.getType().value()); + String result = template.render(data); + FileWriter writer = new FileWriter(filePath(codeModel, "controller") + "/" + + StrUtil.upperFirst(codeModel.getType().value()) + codeModel.getEntity() + + "Controller.java"); + writer.write(result); + } +} diff --git a/src/main/java/com/cool/core/code/CodeModel.java b/src/main/java/com/cool/core/code/CodeModel.java new file mode 100644 index 0000000..a2566fd --- /dev/null +++ b/src/main/java/com/cool/core/code/CodeModel.java @@ -0,0 +1,36 @@ +package com.cool.core.code; + +import lombok.Data; + +/** + * 代码模型 + */ +@Data +public class CodeModel { + /** + * 类型 后台还是对外的接口 admin app + */ + private CodeTypeEnum type; + /** + * 名称 + */ + private String name; + /** + * 模块 + */ + private String module; + + /** + * 子模块 + */ + private String subModule; + + /** + * 实体类 + */ + private String entity; + + public void setEntity(Class entity) { + this.entity = entity.getSimpleName().replace("Entity", ""); + } +} diff --git a/src/main/java/com/cool/core/code/CodeTypeEnum.java b/src/main/java/com/cool/core/code/CodeTypeEnum.java new file mode 100644 index 0000000..873ef49 --- /dev/null +++ b/src/main/java/com/cool/core/code/CodeTypeEnum.java @@ -0,0 +1,21 @@ +package com.cool.core.code; + +/** + * 代码类型 + */ +public enum CodeTypeEnum { + ADMIN("admin", "后端接口"), APP("app", "对外接口"); + + private String value; + + private String des; + + CodeTypeEnum(String value, String des) { + this.value = value; + this.des = des; + } + + public String value() { + return this.value; + } +} diff --git a/src/main/java/com/cool/core/config/CoolProperties.java b/src/main/java/com/cool/core/config/CoolProperties.java new file mode 100644 index 0000000..88e3fe9 --- /dev/null +++ b/src/main/java/com/cool/core/config/CoolProperties.java @@ -0,0 +1,23 @@ +package com.cool.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Configuration; + +/** + * cool的配置 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "cool") +public class CoolProperties { + // 是否自动导入数据 + private Boolean initData = false; + // token配置 + @NestedConfigurationProperty + private TokenProperties token; + // 文件配置 + @NestedConfigurationProperty + private FileProperties file; +} diff --git a/src/main/java/com/cool/core/config/CustomOpenApiResource.java b/src/main/java/com/cool/core/config/CustomOpenApiResource.java new file mode 100644 index 0000000..4cce955 --- /dev/null +++ b/src/main/java/com/cool/core/config/CustomOpenApiResource.java @@ -0,0 +1,36 @@ +package com.cool.core.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.servlet.http.HttpServletRequest; +import org.springdoc.core.customizers.SpringDocCustomizers; +import org.springdoc.core.properties.SpringDocConfigProperties; +import org.springdoc.core.providers.SpringDocProviders; +import org.springdoc.core.service.AbstractRequestService; +import org.springdoc.core.service.GenericResponseService; +import org.springdoc.core.service.OpenAPIService; +import org.springdoc.core.service.OperationService; +import org.springdoc.webmvc.api.OpenApiResource; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.stereotype.Component; + +import java.util.Locale; + +/** + * 自定义 OpenApiResource + */ +@Component +public class CustomOpenApiResource extends OpenApiResource { + + public CustomOpenApiResource(ObjectFactory openAPIBuilderObjectFactory, AbstractRequestService requestBuilder, GenericResponseService responseBuilder, OperationService operationParser, SpringDocConfigProperties springDocConfigProperties, SpringDocProviders springDocProviders, SpringDocCustomizers springDocCustomizers) { + super("springdocDefault", openAPIBuilderObjectFactory, requestBuilder, responseBuilder, operationParser, springDocConfigProperties, springDocProviders, springDocCustomizers); + } + + @Override + protected String getServerUrl(HttpServletRequest request, String apiDocsUrl) { + return ""; + } + + public byte[] getOpenApiJson() throws JsonProcessingException { + return writeJsonValue(getOpenApi(Locale.getDefault())); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/config/FileModeEnum.java b/src/main/java/com/cool/core/config/FileModeEnum.java new file mode 100644 index 0000000..243520b --- /dev/null +++ b/src/main/java/com/cool/core/config/FileModeEnum.java @@ -0,0 +1,28 @@ +package com.cool.core.config; + +/** + * 文件模式 + */ +public enum FileModeEnum { + LOCAL("local", "local", "本地"), CLOUD("cloud", "oss", "云存储"), OTHER("other", "other", "其他"); + + private String value; + + private String type; + + private String des; + + FileModeEnum(String value, String type, String des) { + this.value = value; + this.type = type; + this.des = des; + } + + public String value() { + return this.value; + } + + public String type() { + return this.type; + } +} diff --git a/src/main/java/com/cool/core/config/FileProperties.java b/src/main/java/com/cool/core/config/FileProperties.java new file mode 100644 index 0000000..4af01c0 --- /dev/null +++ b/src/main/java/com/cool/core/config/FileProperties.java @@ -0,0 +1,22 @@ +package com.cool.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Configuration; + +/** + * 文件 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "cool.file") +public class FileProperties { + // 上传模式 + private FileModeEnum mode; + // 上传类型 + private String type; + // 本地文件上传 + @NestedConfigurationProperty + private LocalFileProperties local; +} diff --git a/src/main/java/com/cool/core/config/LocalFileProperties.java b/src/main/java/com/cool/core/config/LocalFileProperties.java new file mode 100644 index 0000000..b901322 --- /dev/null +++ b/src/main/java/com/cool/core/config/LocalFileProperties.java @@ -0,0 +1,23 @@ +package com.cool.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 文件 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "cool.file.local") +public class LocalFileProperties { + + // 跟域名 + private String baseUrl; + + private String uploadPath = "assets/public/upload"; + + public String getAbsoluteUploadFolder() { + return System.getProperty("user.dir") + "/" + uploadPath; + } +} diff --git a/src/main/java/com/cool/core/config/MyBatisFlexConfiguration.java b/src/main/java/com/cool/core/config/MyBatisFlexConfiguration.java new file mode 100644 index 0000000..762c4d2 --- /dev/null +++ b/src/main/java/com/cool/core/config/MyBatisFlexConfiguration.java @@ -0,0 +1,15 @@ +package com.cool.core.config; + +import com.mybatisflex.core.FlexGlobalConfig; +import com.mybatisflex.spring.boot.MyBatisFlexCustomizer; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MyBatisFlexConfiguration implements MyBatisFlexCustomizer { + + @Override + public void customize(FlexGlobalConfig globalConfig) { + // 我们可以在这里进行一些列的初始化配置 + } + +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/config/OssFileProperties.java b/src/main/java/com/cool/core/config/OssFileProperties.java new file mode 100644 index 0000000..1f02b3d --- /dev/null +++ b/src/main/java/com/cool/core/config/OssFileProperties.java @@ -0,0 +1,24 @@ +package com.cool.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 文件 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "cool.file.oss") +public class OssFileProperties { + // accessKeyId + private String accessKeyId; + // accessKeySecret + private String accessKeySecret; + // 文件空间 + private String bucket; + // 地址 + private String endpoint; + // 超时时间 + private Long timeout; +} diff --git a/src/main/java/com/cool/core/config/PluginJson.java b/src/main/java/com/cool/core/config/PluginJson.java new file mode 100644 index 0000000..ad39263 --- /dev/null +++ b/src/main/java/com/cool/core/config/PluginJson.java @@ -0,0 +1,57 @@ +package com.cool.core.config; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.util.Map; + +@Data +public class PluginJson { + /** + * 插件名称 + */ + private String name; + /** + * 插件标识 + */ + private String key; + /** + * 插件钩子,比如替换系统的上传组件,upload + */ + private String hook; + /** + * 版本号 + */ + private String version; + /** + * 插件描述 + */ + private String description; + /** + * 作者 + */ + private String author; + /** + * 插件 logo,建议尺寸 256x256 + */ + private String logo; + /** + * 插件介绍,会展示在插件的详情中 + */ + private String readme; + /** + * 插件配置, 每个插件的配置各不相同 + */ + private Map config; + + /** + * jar包存放路径 + */ + private String jarPath; + + /** + * 同名hook id + */ + @JsonIgnore + private Long sameHookId; +} diff --git a/src/main/java/com/cool/core/config/SwaggerConfig.java b/src/main/java/com/cool/core/config/SwaggerConfig.java new file mode 100644 index 0000000..8772d16 --- /dev/null +++ b/src/main/java/com/cool/core/config/SwaggerConfig.java @@ -0,0 +1,16 @@ +package com.cool.core.config; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; + +@OpenAPIDefinition(info = @Info(title = "COOL-ADMIN", version = "4.0", description = "一个很酷的后台权限管理系统开发框架", contact = @Contact(name = "闪酷科技")), security = @SecurityRequirement(name = "Authorization"), externalDocs = @ExternalDocumentation(description = "参考文档", url = "https://cool-js.com")) +@SecurityScheme(type = SecuritySchemeType.APIKEY, name = "Authorization", in = SecuritySchemeIn.HEADER) +public class SwaggerConfig { + +} diff --git a/src/main/java/com/cool/core/config/TokenProperties.java b/src/main/java/com/cool/core/config/TokenProperties.java new file mode 100644 index 0000000..12cd713 --- /dev/null +++ b/src/main/java/com/cool/core/config/TokenProperties.java @@ -0,0 +1,18 @@ +package com.cool.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * token配置 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "cool.token") +public class TokenProperties { + // token 过期时间 + private Long expire; + // refreshToken 过期时间 + private Long refreshExpire; +} diff --git a/src/main/java/com/cool/core/config/cache/CaffeineConfig.java b/src/main/java/com/cool/core/config/cache/CaffeineConfig.java new file mode 100644 index 0000000..d22204c --- /dev/null +++ b/src/main/java/com/cool/core/config/cache/CaffeineConfig.java @@ -0,0 +1,122 @@ +package com.cool.core.config.cache; + +import com.github.benmanes.caffeine.cache.Caffeine; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.HashMap; +import java.util.Map; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; + +@Slf4j +@Configuration +@EnableCaching +@ConditionalOnProperty(name = "spring.cache.type", havingValue = "CAFFEINE") +public class CaffeineConfig { + + @Value("${spring.cache.file}") + private String cacheFile; + + @Value("${cool.cacheName}") + private String cacheName; + + @Bean + public Caffeine caffeine() { + return Caffeine.newBuilder().maximumSize(10000); + } + + @Bean + public CaffeineCacheManager cacheManager(Caffeine caffeine) { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCaffeine(caffeine); + loadCache(cacheManager); + return cacheManager; + } + + @PostConstruct + public void init() { + File cacheDir = new File(cacheFile).getParentFile(); + if (!cacheDir.exists()) { + if (cacheDir.mkdirs()) { + log.info("Created directory: " + cacheDir.getAbsolutePath()); + } else { + log.error("Failed to create directory: " + cacheDir.getAbsolutePath()); + } + } + } + + private void loadCache(CaffeineCacheManager cacheManager) { + if (cacheManager == null) { + log.error("CacheManager is null"); + return; + } + + if (cacheFile == null || cacheFile.isEmpty()) { + log.error("Cache file path is null or empty"); + return; + } + + File file = new File(cacheFile); + if (!file.exists()) { + log.warn("Cache file does not exist: " + cacheFile); + return; + } + try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file))) { + Map cacheMap = (Map) inputStream.readObject(); + com.github.benmanes.caffeine.cache.Cache caffeineCache = Caffeine.newBuilder() + .build(); + caffeineCache.putAll(cacheMap); + cacheManager.registerCustomCache(cacheName, caffeineCache); + } catch (IOException | ClassNotFoundException e) { + log.error("loadCacheErr", e); + } + } + + @Bean + public CacheLoader cacheLoader(CaffeineCacheManager cacheManager) { + return new CacheLoader(cacheManager, cacheFile); + } + + class CacheLoader { + + private final CaffeineCacheManager cacheManager; + private final String cacheFile; + + public CacheLoader(CaffeineCacheManager cacheManager, String cacheFile) { + this.cacheManager = cacheManager; + this.cacheFile = cacheFile; + } + + @EventListener(ContextClosedEvent.class) + @Scheduled(fixedRate = 10000) + public void persistCache() { + Cache cache = cacheManager.getCache(cacheName); + if (cache != null + && cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) { + Map cacheMap = ((com.github.benmanes.caffeine.cache.Cache) cache + .getNativeCache()).asMap(); + try (ObjectOutputStream outputStream = new ObjectOutputStream( + new FileOutputStream(cacheFile))) { + outputStream.writeObject(new HashMap<>(cacheMap)); + } catch (IOException e) { + log.error("persistCacheErr", e); + } + } + } + } +} diff --git a/src/main/java/com/cool/core/config/cache/RedisConfig.java b/src/main/java/com/cool/core/config/cache/RedisConfig.java new file mode 100644 index 0000000..b892683 --- /dev/null +++ b/src/main/java/com/cool/core/config/cache/RedisConfig.java @@ -0,0 +1,29 @@ +package com.cool.core.config.cache; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; + +@Configuration +@EnableCaching +@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis") +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate( + RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + + @Bean + public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { + return RedisCacheManager.create(redisConnectionFactory); + } +} diff --git a/src/main/java/com/cool/core/eps/CoolEps.java b/src/main/java/com/cool/core/eps/CoolEps.java new file mode 100644 index 0000000..a3bdaf2 --- /dev/null +++ b/src/main/java/com/cool/core/eps/CoolEps.java @@ -0,0 +1,331 @@ +package com.cool.core.eps; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.core.config.CustomOpenApiResource; +import com.mybatisflex.annotation.Table; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +/** + * 实体信息与路径 + */ +@Getter +@Component +@Slf4j +@RequiredArgsConstructor +public class CoolEps { + + @Value("${server.port}") + private int serverPort; + + private Dict entityInfo; + + private JSONObject swaggerInfo; + + @Value("${springdoc.api-docs.enabled}") + private boolean apiDocsEnabled; + + public Dict admin; + + public Dict app; + + final private RequestMappingHandlerMapping requestMappingHandlerMapping; + + final private CustomOpenApiResource customOpenApiResource; + + @Async + public void init() { + if (!apiDocsEnabled) { + return; + } + entityInfo = Dict.create(); + swaggerInfo = swaggerInfo(); + Runnable task = () -> { + entity(); + urls(); + log.info("初始化eps完成,服务启动成功,端口:{}", serverPort); + }; + // ThreadUtil.safeSleep(3000); + ThreadUtil.execute(task); + } + + /** + * 构建所有的url + */ + private void urls() { + Dict admin = Dict.create(); + Dict app = Dict.create(); + Map map = requestMappingHandlerMapping.getHandlerMethods(); + for (Map.Entry methodEntry : map.entrySet()) { + RequestMappingInfo info = methodEntry.getKey(); + HandlerMethod method = methodEntry.getValue(); + String module = getModule(method); + if (StrUtil.isNotEmpty(module)) { + String entityName = getEntity(method.getBeanType()); + String methodPath = getMethodUrl(method); + String prefix = Objects.requireNonNull(getUrl(info)) + .replaceFirst("(?s)(.*)" + methodPath, "$1"); + Dict result = Dict.create(); + int type = 0; + if (prefix.startsWith("/admin")) { + result = admin; + } + if (prefix.startsWith("/app")) { + result = app; + type = 1; + } + if (result.get(module) == null) { + result.set(module, new ArrayList()); + } + + List urls = result.getBean(module); + Dict item = CollUtil.findOne(urls, dict -> { + if (dict != null) { + return dict.getStr("module").equals(module) + && dict.getStr("controller") + .equals(method.getBeanType().getSimpleName()); + } else { + return false; + } + }); + if (item != null) { + item.set("api", apis(prefix, methodPath, item.getBean("api"))); + } else { + item = Dict.create(); + item.set("controller", method.getBeanType().getSimpleName()); + item.set("module", module); + item.set("name", entityName); + item.set("api", new ArrayList()); + item.set("prefix", prefix); + item.set("columns", entityInfo.get(entityName)); + item.set("api", apis(prefix, methodPath, item.getBean("api"))); + urls.add(item); + } + if (type == 0) { + admin.set(module, urls); + } + if (type == 1) { + app.set(module, urls); + } + + } + } + this.admin = admin; + this.app = app; + + } + + /** + * 设置所有的api + * + * @param prefix 路由前缀 + * @param methodPath 方法路由 + * @param list api列表 + * @return api列表 + */ + private List apis(String prefix, String methodPath, List list) { + Dict item = Dict.create(); + item.set("method", ""); + item.set("path", methodPath); + item.set("summary", ""); + item.set("tag", ""); + item.set("dts", new Object()); + setSwaggerInfo(item, prefix + methodPath); + list.add(item); + return list; + } + + /** + * 设置swagger相关信息 + * + * @param item 信息载体 + * @param url url地址 + */ + private void setSwaggerInfo(Dict item, String url) { + JSONObject paths = swaggerInfo.getJSONObject("paths"); + JSONObject urlInfo = paths.getJSONObject(url); + String method = urlInfo.keySet().iterator().next(); + JSONObject methodInfo = urlInfo.getJSONObject(method); + item.set("dts", methodInfo); + item.set("method", method); + item.set("summary", methodInfo.getStr("summary")); + item.set("description", methodInfo.get("description")); + } + + /** + * 获得方法的url地址 + * + * @param handlerMethod 方法 + * @return 方法url地址 + */ + private String getMethodUrl(HandlerMethod handlerMethod) { + String url = null; + Method method = handlerMethod.getMethod(); + Annotation[] annotations = method.getDeclaredAnnotations(); + + for (Annotation annotation : annotations) { + Class annotationType = annotation.annotationType(); + if (annotationType.getName().contains("org.springframework.web.bind.annotation")) { + Map attributes = Arrays.stream(annotationType.getDeclaredMethods()) + .collect(Collectors.toMap(Method::getName, m -> { + try { + return m.invoke(annotation); + } catch (Exception e) { + throw new IllegalStateException("Failed to access annotation attribute", + e); + } + })); + + if (attributes.containsKey("value")) { + url = ((String[]) attributes.get("value"))[0]; + } + break; + } + } + + return url; + } + + /** + * 获得url地址 + * + * @param info 路由信息 + * @return url地址 + */ + private String getUrl(RequestMappingInfo info) { + if (info.getPathPatternsCondition() == null) { + return null; + } + Set paths = info.getPathPatternsCondition().getPatternValues(); + return paths.iterator().next(); + } + + /** + * 获得模块 + * + * @param method 方法 + * @return 模块 + */ + private String getModule(HandlerMethod method) { + String beanName = method.getBeanType().getName(); + String[] beanNames = beanName.split("[.]"); + int index = ArrayUtil.indexOf(beanNames, "modules"); + if (index > 0) { + return beanNames[index + 1]; + } + return null; + } + + /** + * 获得swagger的json信息 + */ + private JSONObject swaggerInfo() { + try { + byte[] bytes = customOpenApiResource.getOpenApiJson(); + return JSONUtil.parseObj(new String(bytes)); + } catch (Exception e) { + return new JSONObject(); + } + } + + /** + * 获得Controller上的实体类型 + * + * @param controller Controller类 + * @return 实体名称 + */ + private String getEntity(Class controller) { + try { + Type type = ((ParameterizedType) SpringUtil.getBean(controller).getClass() + .getGenericSuperclass()) + .getActualTypeArguments()[1]; + String[] names = type.getTypeName().split("[.]"); + return names[names.length - 1]; + } catch (Exception e) { + return ""; + } + } + + private void entity() { + // 扫描所有的实体类 + Set> classes = ClassUtil.scanPackageByAnnotation("", Table.class); + classes.forEach(e -> { + // 获得属性 + Field[] fields = ClassUtil.getDeclaredFields(e); + List columns = columns(fields); + entityInfo.set(e.getSimpleName(), columns); + }); + } + + /** + * 获得所有的列 + * + * @param fields 字段名 + * @return 所有的列 + */ + private List columns(Field[] fields) { + List dictList = new ArrayList<>(); + for (Field field : fields) { + Dict dict = Dict.create(); + ColumnDefine columnInfo = AnnotatedElementUtils.findMergedAnnotation(field, + ColumnDefine.class); + if (columnInfo == null) { + continue; + } + dict.set("comment", columnInfo.comment()); + dict.set("length", columnInfo.length()); + dict.set("propertyName", field.getName()); + dict.set("type", matchType(field.getType().getName())); + dict.set("nullable", !columnInfo.notNull()); + dictList.add(dict); + } + return dictList; + } + + /** + * java类型转换成JavaScript对应的类型 + * + * @param type 类型 + * @return JavaScript类型 + */ + private String matchType(String type) { + return switch (type) { + case "java.lang.Boolean" -> "boolean"; + case "java.lang.Long", "java.lang.Integer", "java.lang.Short", "java.lang.Float", + "java.lang.Double" -> "number"; + case "java.util.Date" -> "date"; + default -> "string"; + }; + } +} diff --git a/src/main/java/com/cool/core/eps/EpsEvent.java b/src/main/java/com/cool/core/eps/EpsEvent.java new file mode 100644 index 0000000..1e40b01 --- /dev/null +++ b/src/main/java/com/cool/core/eps/EpsEvent.java @@ -0,0 +1,26 @@ +package com.cool.core.eps; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 事件监听 + */ +@Slf4j +@Component +@Profile({"local"}) +@RequiredArgsConstructor +public class EpsEvent { + + final private CoolEps coolEps; + + @EventListener + public void onApplicationEvent(ApplicationReadyEvent event) { + coolEps.init(); + log.info("构建eps信息"); + } +} diff --git a/src/main/java/com/cool/core/exception/CoolException.java b/src/main/java/com/cool/core/exception/CoolException.java new file mode 100644 index 0000000..90498a5 --- /dev/null +++ b/src/main/java/com/cool/core/exception/CoolException.java @@ -0,0 +1,43 @@ +package com.cool.core.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 自定义异常处理 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class CoolException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private String msg; + private int code = 500; + private Object data; + + public CoolException(String msg) { + super(msg); + this.msg = msg; + } + + public CoolException(String msg, Throwable e) { + super(msg, e); + this.msg = msg; + } + + public CoolException(String msg, int code) { + super(msg); + this.msg = msg; + this.code = code; + } + + public CoolException(String msg, int code, Throwable e) { + super(msg, e); + this.msg = msg; + this.code = code; + } + + public CoolException(Object data) { + this.data = data; + } +} diff --git a/src/main/java/com/cool/core/exception/CoolExceptionHandler.java b/src/main/java/com/cool/core/exception/CoolExceptionHandler.java new file mode 100644 index 0000000..39cd40e --- /dev/null +++ b/src/main/java/com/cool/core/exception/CoolExceptionHandler.java @@ -0,0 +1,61 @@ +package com.cool.core.exception; + +import cn.hutool.core.util.ObjUtil; +import com.cool.core.request.R; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 异常处理器 + */ +@RestControllerAdvice +@Slf4j +public class CoolExceptionHandler { + + @ExceptionHandler(CoolException.class) + public R handleRRException(CoolException e) { + R r = new R(); + if (ObjUtil.isNotEmpty(e.getData())) { + r.put("data", e.getData()); + } else { + r.put("code", e.getCode()); + r.put("message", e.getMessage()); + } + return r; + } + + @ExceptionHandler(DuplicateKeyException.class) + public R handleDuplicateKeyException(DuplicateKeyException e) { + log.error(e.getMessage(), e); + return R.error("已存在该记录或值不能重复"); + } + + @ExceptionHandler(BadCredentialsException.class) + public R handleBadCredentialsException(BadCredentialsException e) { + log.error(e.getMessage(), e); + return R.error("账户密码不正确"); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public R handleHttpRequestMethodNotSupportedException( + HttpRequestMethodNotSupportedException e) { + log.error(e.getMessage(), e); + return R.error("不支持该请求方式,请区分POST、GET等请求方式是否正确"); + } + + @ExceptionHandler(IllegalArgumentException.class) + public R handleIllegalArgumentException(IllegalArgumentException e) { + log.error(e.getMessage(), e); + return R.error(e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public R handleException(Exception e) { + log.error(e.getMessage(), e); + return R.error(); + } +} diff --git a/src/main/java/com/cool/core/exception/CoolPreconditions.java b/src/main/java/com/cool/core/exception/CoolPreconditions.java new file mode 100644 index 0000000..ebb6ade --- /dev/null +++ b/src/main/java/com/cool/core/exception/CoolPreconditions.java @@ -0,0 +1,81 @@ +package com.cool.core.exception; + +import cn.hutool.core.util.ObjectUtil; +import lombok.Getter; +import lombok.Setter; + +/** + * 校验处理 + */ +public class CoolPreconditions { + + /** + * 条件如果为真 就抛异常 如 CoolPreconditions.check(StrUtil.isEmptyIfStr(name), 500, + * "名称不能为空"); name 字段如果为 null或空字符串,就抛异常 + */ + public static void check(boolean flag, int code, String message, Object... arguments) { + if (flag) { + throw new CoolException(formatMessage(message, arguments), code); + } + } + + public static void check(boolean flag, String message, Object... arguments) { + if (flag) { + throw new CoolException(formatMessage(message, arguments)); + } + } + + public static void alwaysThrow(String message, Object... arguments) { + throw new CoolException(formatMessage(message, arguments)); + } + + /** + * 返回data + */ + public static void returnData(boolean flag, Object data) { + if (flag) { + throw new CoolException(data); + } + } + + public static void returnData(Object data) { + returnData(true, data); + } + + /** + * 对象如果为空 就抛异常 + */ + public static void checkEmpty(Object object, String message, Object... arguments) { + check(ObjectUtil.isEmpty(object), formatMessage(message, arguments)); + } + + private static String formatMessage(String messagePattern, Object... arguments) { + StringBuilder sb = new StringBuilder(); + int argumentIndex = 0; + int placeholderIndex = messagePattern.indexOf("{}"); + while (placeholderIndex != -1) { + sb.append(messagePattern, 0, placeholderIndex); + if (argumentIndex < arguments.length) { + sb.append(arguments[argumentIndex++]); + } else { + sb.append("{}"); // 如果参数不足,保留原样 + } + messagePattern = messagePattern.substring(placeholderIndex + 2); + placeholderIndex = messagePattern.indexOf("{}"); + } + sb.append(messagePattern); // 添加剩余部分 + return sb.toString(); + } + + @Setter + @Getter + public static class ReturnData { + private Integer type; + private String message; + + public ReturnData(Integer type, String message, Object... arguments) { + this.type = type; + this.message = formatMessage(message, arguments); + } + } +} diff --git a/src/main/java/com/cool/core/file/FileUploadStrategyFactory.java b/src/main/java/com/cool/core/file/FileUploadStrategyFactory.java new file mode 100644 index 0000000..34a9063 --- /dev/null +++ b/src/main/java/com/cool/core/file/FileUploadStrategyFactory.java @@ -0,0 +1,57 @@ +package com.cool.core.file; + +import static com.cool.core.plugin.consts.PluginConsts.uploadHook; + +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ObjUtil; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.file.strategy.FileUploadStrategy; +import com.cool.core.plugin.service.CoolPluginService; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@Component +@RequiredArgsConstructor +public class FileUploadStrategyFactory { + + final private ApplicationContext applicationContext; + + final private CoolPluginService coolPluginService; + + public FileUploadStrategy getStrategy() { + PluginInfoEntity pluginInfoEntity = coolPluginService.getPluginInfoEntityByHook(uploadHook); + return getStrategy(pluginInfoEntity); + } + + private FileUploadStrategy getStrategy(PluginInfoEntity pluginInfoEntity) { + if (ObjUtil.isEmpty(pluginInfoEntity)) { + return applicationContext.getBean("localFileUploadStrategy", FileUploadStrategy.class); + } + return applicationContext.getBean("ossFileUploadStrategy", FileUploadStrategy.class); + } + + public Object upload(MultipartFile[] files, HttpServletRequest request) { + PluginInfoEntity pluginInfoEntity = coolPluginService.getPluginInfoEntityByHook(uploadHook); + try { + return getStrategy(pluginInfoEntity).upload(files, request, pluginInfoEntity); + } catch (IOException e) { + log.error("上传文件失败", e); + CoolPreconditions.alwaysThrow("上传文件失败 {}", e.getMessage()); + } + return null; + } + + public Object getMode() { + FileUploadStrategy strategy = getStrategy(); + UpLoadModeType upLoadModeType = strategy.getMode(); + return Dict.create().set("mode", upLoadModeType.getMode().value()) + .set("type", upLoadModeType.getType()); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/file/UpLoadModeType.java b/src/main/java/com/cool/core/file/UpLoadModeType.java new file mode 100644 index 0000000..eb99140 --- /dev/null +++ b/src/main/java/com/cool/core/file/UpLoadModeType.java @@ -0,0 +1,26 @@ +package com.cool.core.file; + +import com.cool.core.config.FileModeEnum; +import lombok.Data; + +/** + * 上传模式类型 + */ +@Data +public class UpLoadModeType { + + /** + * 模式 + */ + private FileModeEnum mode; + + /** + * 类型 + */ + private String type; + + public UpLoadModeType(FileModeEnum mode) { + this.mode = mode; + this.type = mode.type(); + } +} diff --git a/src/main/java/com/cool/core/file/strategy/FileUploadStrategy.java b/src/main/java/com/cool/core/file/strategy/FileUploadStrategy.java new file mode 100644 index 0000000..fa72664 --- /dev/null +++ b/src/main/java/com/cool/core/file/strategy/FileUploadStrategy.java @@ -0,0 +1,39 @@ +package com.cool.core.file.strategy; + +import com.cool.core.file.UpLoadModeType; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public interface FileUploadStrategy { + + /** + * 文件上传 + */ + Object upload(MultipartFile[] files, HttpServletRequest request, PluginInfoEntity pluginInfoEntity) + throws IOException; + + /** + * 文件上传模式 + * + * @return 上传模式 + */ + UpLoadModeType getMode(); + + default boolean isAbsolutePath(String pathStr) { + Path path = Paths.get(pathStr); + return path.isAbsolute(); + } + + default String getExtensionName(String fileName) { + if (fileName.contains(".")) { + String[] names = fileName.split("[.]"); + return "." + names[names.length - 1]; + } + return ""; + } +} diff --git a/src/main/java/com/cool/core/file/strategy/LocalFileUploadStrategy.java b/src/main/java/com/cool/core/file/strategy/LocalFileUploadStrategy.java new file mode 100644 index 0000000..904d0be --- /dev/null +++ b/src/main/java/com/cool/core/file/strategy/LocalFileUploadStrategy.java @@ -0,0 +1,78 @@ +package com.cool.core.file.strategy; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import com.cool.core.config.FileModeEnum; +import com.cool.core.config.LocalFileProperties; +import com.cool.core.exception.CoolException; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.file.UpLoadModeType; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import jakarta.servlet.http.HttpServletRequest; + +import java.io.File; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component("localFileUploadStrategy") +@RequiredArgsConstructor +public class LocalFileUploadStrategy implements FileUploadStrategy { + + final private LocalFileProperties localFileProperties; + + /** + * 上传文件 + * + * @param files 上传的文件 + * @return 文件路径 + */ + @Override + public Object upload(MultipartFile[] files, HttpServletRequest request, + PluginInfoEntity pluginInfoEntity) { + CoolPreconditions.check(StrUtil.isEmpty(localFileProperties.getBaseUrl()), + "filePath 或 baseUrl 未配置"); + try { + List fileUrls = new ArrayList<>(); + String baseUrl = localFileProperties.getBaseUrl(); + // 绝对路径 + String filePath = System.getProperty("user.dir"); + String date = DateUtil.format(new Date(), + DatePattern.PURE_DATE_PATTERN); + String relativePath = + "/" + localFileProperties.getUploadPath() + "/" + date; + + FileUtil.mkdir(filePath + relativePath); + for (MultipartFile file : files) { + // 保存文件 + String fileName = StrUtil.uuid().replaceAll("-", "") + getExtensionName( + Objects.requireNonNull(file.getOriginalFilename())); + file.transferTo(new File(filePath + "/" + relativePath + + "/" + fileName)); + fileUrls.add(baseUrl + "/" + date + "/" + fileName); + } + if (fileUrls.size() == 1) { + return fileUrls.get(0); + } + return fileUrls; + } catch (Exception e) { + throw new CoolException("文件上传失败", e); + } + } + + /** + * 文件上传模式 + * + * @return 上传模式 + */ + public UpLoadModeType getMode() { + return new UpLoadModeType(FileModeEnum.LOCAL); + } +} diff --git a/src/main/java/com/cool/core/file/strategy/OssFileUploadStrategy.java b/src/main/java/com/cool/core/file/strategy/OssFileUploadStrategy.java new file mode 100644 index 0000000..a9e4384 --- /dev/null +++ b/src/main/java/com/cool/core/file/strategy/OssFileUploadStrategy.java @@ -0,0 +1,26 @@ +package com.cool.core.file.strategy; + +import com.cool.core.config.FileModeEnum; +import com.cool.core.file.UpLoadModeType; +import com.cool.core.util.CoolPluginInvokers; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@Component("ossFileUploadStrategy") +public class OssFileUploadStrategy implements FileUploadStrategy { + + @Override + public Object upload(MultipartFile[] files, HttpServletRequest request, PluginInfoEntity pluginInfoEntity) + throws IOException { + return CoolPluginInvokers.invokePlugin(pluginInfoEntity.getKey()); + } + + @Override + public UpLoadModeType getMode() { + return new UpLoadModeType(FileModeEnum.CLOUD); + } +} diff --git a/src/main/java/com/cool/core/init/CoolPluginInit.java b/src/main/java/com/cool/core/init/CoolPluginInit.java new file mode 100644 index 0000000..36ca4ff --- /dev/null +++ b/src/main/java/com/cool/core/init/CoolPluginInit.java @@ -0,0 +1,24 @@ +package com.cool.core.init; + +import com.cool.core.plugin.service.CoolPluginService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +/** + * 历史安装过的插件执行初始化 + **/ +@Slf4j +@Component +@RequiredArgsConstructor +public class CoolPluginInit implements ApplicationRunner { + + final private CoolPluginService coolPluginService; + + @Override + public void run(ApplicationArguments args) { + coolPluginService.init(); + } +} diff --git a/src/main/java/com/cool/core/init/DBFromJsonInit.java b/src/main/java/com/cool/core/init/DBFromJsonInit.java new file mode 100644 index 0000000..68c0e5c --- /dev/null +++ b/src/main/java/com/cool/core/init/DBFromJsonInit.java @@ -0,0 +1,218 @@ +package com.cool.core.init; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.core.base.service.MapperProviderService; +import com.cool.core.util.EntityUtils; +import com.cool.modules.base.entity.sys.BaseSysConfEntity; +import com.cool.modules.base.entity.sys.BaseSysMenuEntity; +import com.cool.modules.base.service.sys.BaseSysConfService; +import com.cool.modules.base.service.sys.BaseSysMenuService; +import com.mybatisflex.core.BaseMapper; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Component; +import org.springframework.context.ApplicationEventPublisher; + +/** + * 数据库初始数据初始化 在 classpath:cool/data/db 目录下创建.json文件 并定义表数据, 由该类统一执行初始化 + **/ +@Slf4j +@Component +@RequiredArgsConstructor +public class DBFromJsonInit implements ApplicationRunner { + + final private BaseSysConfService baseSysConfService; + + final private BaseSysMenuService baseSysMenuService; + + final private MapperProviderService mapperProviderService; + + final private ApplicationEventPublisher eventPublisher; + + @Value("${cool.initData}") + private boolean initData; + + @Override + public void run(ApplicationArguments args) { + if (!initData) { + return; + } + // 初始化自定义的数据 + extractedDb(); + // 初始化菜单数据 + extractedMenu(); + // 发送数据库初始化完成事件 + eventPublisher.publishEvent(new DbInitCompleteEvent(this)); + log.info("数据初始化完成!"); + } + + @Getter + public static class DbInitCompleteEvent { + private final Object source; + + public DbInitCompleteEvent(Object source) { + this.source = source; + } + + } + + /** + * 解析插入业务数据 + */ + private void extractedDb() { + try { + // 加载 JSON 文件 + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources("classpath:cool/data/db/*.json"); + // 遍历所有.json文件 + analysisResources(resources); + } catch (Exception e) { + log.error("Failed to initialize data", e); + } + } + + private void analysisResources(Resource[] resources) + throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + String prefix = "db_"; + for (Resource resource : resources) { + File resourceFile = new File(resource.getURL().getFile()); + String fileName = prefix + resourceFile.getName(); + String value = baseSysConfService.getValue(fileName); + if (StrUtil.isNotEmpty(value)) { + log.info("{} 业务数据已初始化过...", fileName); + continue; + } + String jsonStr = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8); + JSONObject jsonObject = JSONUtil.parseObj(jsonStr); + // 遍历 JSON 文件中的数据 + analysisJson(jsonObject); + + BaseSysConfEntity baseSysUserEntity = new BaseSysConfEntity(); + baseSysUserEntity.setCKey(fileName); + baseSysUserEntity.setCValue("success"); + // 当前文件已加载 + baseSysConfService.add(baseSysUserEntity); + log.info("{} 业务数据初始化成功...", fileName); + } + } + + private void analysisJson(JSONObject jsonObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + Map> tableMap = EntityUtils.findTableMap(); + for (String tableName : jsonObject.keySet()) { + JSONArray records = jsonObject.getJSONArray(tableName); + // 根据表名生成实体类名和 Mapper 接口名 + Class entityClass = tableMap.get(tableName); + BaseMapper baseMapper = mapperProviderService.getMapperByEntityClass(entityClass); + // 插入 + insertList(baseMapper, entityClass, records); + } + } + + /** + * 插入列表数据 + */ + private void insertList(BaseMapper baseMapper, Class entityClass, + JSONArray records) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + // 插入数据 + List list = new ArrayList<>(); + for (int i = 0; i < records.size(); i++) { + JSONObject record = records.getJSONObject(i); + Object entity = JSONUtil.toBean(record, entityClass); + Method getIdMethod = entityClass.getMethod("getId"); + Object id = getIdMethod.invoke(entity); + if (ObjUtil.isNotEmpty(id) && ObjUtil.isNotEmpty( + baseMapper.selectOneById((Long) id))) { + // 数据库已经有值了 + continue; + } + list.add(entity); + } + baseMapper.insertBatch(list); + } + + /** + * 解析插入菜单数据 + */ + public void extractedMenu() { + try { + String prefix = "menu_"; + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources("classpath:cool/data/menu/*.json"); + // 遍历所有.json文件 + for (Resource resource : resources) { + File resourceFile = new File(resource.getURL().getFile()); + String fileName = prefix + resourceFile.getName(); + String value = baseSysConfService.getValue(fileName); + if (StrUtil.isNotEmpty(value)) { + log.info("{} 菜单数据已初始化过...", fileName); + continue; + } + analysisResources(resource, fileName); + } + } catch (Exception e) { + log.error("Failed to initialize data", e); + } + } + + private void analysisResources(Resource resource, String fileName) throws IOException { + String jsonStr = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8); + + // 使用 解析 JSON 字符串 + JSONArray jsonArray = JSONUtil.parseArray(jsonStr); + + // 遍历 JSON 数组 + for (Object obj : jsonArray) { + JSONObject jsonObj = (JSONObject) obj; + // 将 JSON 对象转换为 Menu 对象 + parseMenu(jsonObj, null); + } + BaseSysConfEntity baseSysUserEntity = new BaseSysConfEntity(); + baseSysUserEntity.setCKey(fileName); + baseSysUserEntity.setCValue("success"); + // 当前文件已加载 + baseSysConfService.add(baseSysUserEntity); + log.info("{} 菜单数据初始化成功...", fileName); + } + + // 递归解析 JSON 对象为 Menu 对象 + private void parseMenu(JSONObject jsonObj, BaseSysMenuEntity parentMenuEntity) { + BaseSysMenuEntity menuEntity = BeanUtil.copyProperties(jsonObj, BaseSysMenuEntity.class); + if (ObjUtil.isNotEmpty(parentMenuEntity)) { + menuEntity.setParentName(parentMenuEntity.getName()); + menuEntity.setParentId(parentMenuEntity.getId()); + } + baseSysMenuService.add(menuEntity); + // 递归处理子菜单 + JSONArray childMenus = jsonObj.getJSONArray("childMenus"); + if (childMenus != null) { + for (Object obj : childMenus) { + JSONObject childObj = (JSONObject) obj; + parseMenu(childObj, menuEntity); + } + } + } +} diff --git a/src/main/java/com/cool/core/plugin/config/DynamicJarClassLoader.java b/src/main/java/com/cool/core/plugin/config/DynamicJarClassLoader.java new file mode 100644 index 0000000..08a6797 --- /dev/null +++ b/src/main/java/com/cool/core/plugin/config/DynamicJarClassLoader.java @@ -0,0 +1,55 @@ +package com.cool.core.plugin.config; + +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; + +/** + * * 自定义类加载器 + */ +@Slf4j +public class DynamicJarClassLoader extends URLClassLoader { + + private final Map> loadedClasses = new ConcurrentHashMap<>(); + + public DynamicJarClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + // 从已加载的类集合中获取指定名称的类 + Class clazz = loadedClasses.get(name); + if (clazz != null) { + return clazz; + } + clazz = super.findClass(name); + // 将加载的类添加到已加载的类集合中 + loadedClasses.put(name, clazz); + return clazz; + } + + public void unload() { + try { + for (Map.Entry> entry : loadedClasses.entrySet()) { + // 从已加载的类集合中移除该类 + String className = entry.getKey(); + loadedClasses.remove(className); + try { + // 调用该类的destory方法,回收资源 + Class clazz = entry.getValue(); + Method destory = clazz.getDeclaredMethod("destory"); + destory.invoke(clazz); + } catch (NoClassDefFoundError | Exception ignored) { + } + } + // 从其父类加载器的加载器层次结构中移除该类加载器 + close(); + } catch (Exception e) { + log.error("unload error", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/plugin/consts/PluginConsts.java b/src/main/java/com/cool/core/plugin/consts/PluginConsts.java new file mode 100644 index 0000000..d0d1ddb --- /dev/null +++ b/src/main/java/com/cool/core/plugin/consts/PluginConsts.java @@ -0,0 +1,27 @@ +package com.cool.core.plugin.consts; + +/** + * 常量工具 + */ +public interface PluginConsts { + + /** + * 上传文件hook + */ + String uploadHook = "upload"; + + /** + * 插件调用入口方法 + */ + String invokePluginMethodName = "invokePlugin"; + + /** + * 设置插件信息 + */ + String setPluginJson = "setPluginJson"; + + /** + * 设置 ApplicationContext,使得插件能够调用主应用spring容器中的bean + */ + String setApplicationContext = "setApplicationContext"; +} diff --git a/src/main/java/com/cool/core/plugin/service/CoolPluginService.java b/src/main/java/com/cool/core/plugin/service/CoolPluginService.java new file mode 100644 index 0000000..4155a7c --- /dev/null +++ b/src/main/java/com/cool/core/plugin/service/CoolPluginService.java @@ -0,0 +1,273 @@ +package com.cool.core.plugin.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.codec.Base64Encoder; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import com.cool.core.config.PluginJson; +import com.cool.core.exception.CoolException; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.plugin.config.DynamicJarClassLoader; +import com.cool.core.util.CoolPluginInvokers; +import com.cool.core.util.MapExtUtil; +import com.cool.core.util.PathUtils; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import com.cool.modules.plugin.service.PluginInfoService; +import com.mybatisflex.core.query.QueryWrapper; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +/** + * 插件服务类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CoolPluginService { + + final private DynamicJarLoaderService dynamicJarLoaderService; + + final private PluginInfoService pluginInfoService; + + @Value("${cool.pluginPath}") + private String pluginPath; + + public void init() { + List list = pluginInfoService + .list(QueryWrapper + .create().select(PluginInfoEntity::getId, PluginInfoEntity::getPluginJson, + PluginInfoEntity::getKey, PluginInfoEntity::getName) + .eq(PluginInfoEntity::getStatus, 1)); + if (ObjUtil.isEmpty(list)) { + log.info("没有可初始化的插件"); + return; + } + list.forEach(this::initInstall); + } + + /** + * 系统启动初始化安装插件 + */ + private void initInstall(PluginInfoEntity entity) { + PluginJson pluginJson = entity.getPluginJson(); + File file = new File(pluginJson.getJarPath()); + // 检查路径是否存在 + if (!file.exists()) { + PluginInfoEntity pluginInfoEntity = pluginInfoService.getById(entity.getId()); + FileUtil.writeBytes(pluginInfoEntity.getJarFile(), file); + } + file = new File(pluginJson.getJarPath()); + if (file.exists()) { + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + try { + dynamicJarLoaderService.install(pluginJson.getJarPath(), true); + // 设置配置 + CoolPluginInvokers.setPluginJson(entity.getKey(), entity); + } catch (Exception e) { + log.error("初始化{}插件失败", entity.getName(), e); + } finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + } + + /** + * 安装jar + */ + public void install(MultipartFile file, boolean force) { + String fileName; + File jarFile = null; + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + try { + String pathStr = pluginPath; + if (!PathUtils.isAbsolutePath(pluginPath)) { + // 相对路径 + pathStr = System.getProperty("user.dir") + "/" + pluginPath; + } + // 将路径字符串转换为 Path 对象 + Path path = Paths.get(pathStr); + // 检查路径是否存在 + if (!Files.exists(path)) { + // 如果路径不存在,则创建目录(包括父目录) + Files.createDirectories(path); + } + fileName = + path + "/" + System.currentTimeMillis() + "_" + file.getOriginalFilename() + ".jar"; + jarFile = new File(fileName); + file.transferTo(jarFile); + // 加载jar + PluginJson pluginJson = dynamicJarLoaderService.install(fileName, force); + // 保存插件信息入库 + savePluginInfo(pluginJson, fileName, jarFile, force); + // 把 ApplicationContext 对象传递打插件类中,使其在插件中也能正常使用spring bean对象 + CoolPluginInvokers.setApplicationContext(pluginJson.getKey()); + } catch (CoolException e) { + FileUtil.del(jarFile); + throw e; + } catch (Exception e) { + log.error("插件安装失败", e); + CoolPreconditions.alwaysThrow("插件安装失败", e); + } finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + + /** + * 卸载 + */ + public void uninstall(Long id) { + PluginInfoEntity pluginInfoEntity = pluginInfoService.getPluginInfoEntityByIdNoJarFile(id); + CoolPreconditions.checkEmpty(pluginInfoEntity, "插件不存在"); + if (dynamicJarLoaderService.uninstall(pluginInfoEntity.getKey())) { + boolean flag = pluginInfoEntity.removeById(); + if (flag) { + FileUtil.del(pluginInfoEntity.getPluginJson().getJarPath()); + } + } + } + + /** + * 保存插件信息 + */ + private void savePluginInfo(PluginJson pluginJson, String fileName, File jarFile, + boolean force) { + CoolPreconditions.checkEmpty(pluginJson, "插件安装失败"); + pluginJson.setJarPath(fileName); + PluginInfoEntity pluginInfo = new PluginInfoEntity(); + BeanUtil.copyProperties(pluginJson, pluginInfo); + if (ObjUtil.isNotEmpty(pluginJson.getLogo())) { + DynamicJarClassLoader dynamicJarClassLoader = dynamicJarLoaderService + .getDynamicJarClassLoader(pluginJson.getKey()); + InputStream inputStream = dynamicJarClassLoader.getResourceAsStream( + pluginJson.getLogo()); + if (ObjUtil.isNotEmpty(inputStream)) { + pluginInfo.setLogo(Base64Encoder.encode(IoUtil.readBytes(inputStream))); + } + } + if (ObjUtil.isNotEmpty(pluginJson.getReadme())) { + DynamicJarClassLoader dynamicJarClassLoader = dynamicJarLoaderService + .getDynamicJarClassLoader(pluginJson.getKey()); + InputStream inputStream = dynamicJarClassLoader.getResourceAsStream( + pluginJson.getReadme()); + if (ObjUtil.isNotEmpty(inputStream)) { + pluginInfo.setReadme(StrUtil.str(IoUtil.readBytes(inputStream), "UTF-8")); + } + } + pluginInfo.setKey(pluginJson.getKey()); + pluginInfo.setPluginJson(pluginJson); + // 转二进制 + pluginInfo.setJarFile(FileUtil.readBytes(jarFile)); + + if (force) { + CopyOptions options = CopyOptions.create().setIgnoreNullValue(true); + if (ObjUtil.isNotEmpty(pluginJson.getSameHookId())) { + // 存在同名,已强制安装,需将原插件关闭 + PluginInfoEntity sameHookPlugin = new PluginInfoEntity(); + sameHookPlugin.setStatus(0); + sameHookPlugin.setId(pluginJson.getSameHookId()); + updatePlugin(sameHookPlugin); + } + // 通过key 找到id + PluginInfoEntity one = pluginInfoService.getByKeyNoJarFile(pluginJson.getKey()); + if (ObjUtil.isNotEmpty(one)) { + // 重新加载配置不更新 + pluginInfo.setConfig(one.getConfig()); + pluginInfo.getPluginJson().setConfig(one.getConfig()); + BeanUtil.copyProperties(pluginInfo, one, options); + one.updateById(); + // 重新安装了调用设置插件历史配置信息 + CoolPluginInvokers.setPluginJson(pluginJson.getKey(), one); + return; + } + } + pluginInfo.save(); + } + + public void updatePlugin(PluginInfoEntity entity) { + PluginInfoEntity dbPluginInfoEntity = pluginInfoService.getPluginInfoEntityByIdNoJarFile( + entity.getId()); + boolean updateConfig = false; + if (!MapExtUtil.compareMaps(entity.getConfig(), dbPluginInfoEntity.getConfig())) { + // 不一致,说明更新了配置 + entity.getPluginJson().setConfig(entity.getConfig()); + updateConfig = true; + } + if (!ObjUtil.equals(entity.getStatus(), dbPluginInfoEntity.getStatus())) { + // 更新状态 + updateConfig = updateStatus(entity, dbPluginInfoEntity, updateConfig); + } + if (updateConfig) { + // 更新配置 + CoolPluginInvokers.setPluginJson(getKeyById(entity.getId()), entity); + } + pluginInfoService.update(entity); + } + + /** + * 更新插件状态 + */ + private boolean updateStatus(PluginInfoEntity entity, PluginInfoEntity dbPluginInfoEntity, + boolean updateConfig) { + // 更新状态 + Integer status = entity.getStatus(); + if (ObjUtil.equals(status, 1)) { + if (ObjUtil.isNotEmpty(dbPluginInfoEntity.getHook())) { + // 查找是否有同名hook,有同名hook,如果状态为开启不允许在开启,需先关闭原来 + PluginInfoEntity hookPlugin = pluginInfoService + .getPluginInfoEntityByHookNoJarFile(dbPluginInfoEntity.getHook()); + if (ObjUtil.isNotEmpty(hookPlugin)) { + CoolPreconditions.check( + !ObjUtil.equals(hookPlugin.getKey(), dbPluginInfoEntity.getKey()) + && ObjUtil.equals(hookPlugin.getStatus(), 1), + "插件已存在相同hook: {},请选关闭{}插件,在开启当前插件(同名hook,只能有一个插件开启)", + hookPlugin.getHook(), + hookPlugin.getName()); + } + } + // 充关闭置为开启,触发重新加载jar + initInstall(dbPluginInfoEntity); + updateConfig = false; + } else if (ObjUtil.equals(status, 0)) { + // 插件关闭 卸载jar + dynamicJarLoaderService.uninstall(dbPluginInfoEntity.getKey()); + } + return updateConfig; + } + + /** + * 通过hook获取插件 + */ + public PluginInfoEntity getPluginInfoEntityByHook(String hook) { + return pluginInfoService.getPluginInfoEntityByHookNoJarFile(hook); + } + + /** + * 通过id 获取key + */ + private String getKeyById(Long id) { + PluginInfoEntity one = pluginInfoService.getPluginInfoEntityByIdNoJarFile(id); + if (ObjUtil.isNotEmpty(one)) { + return one.getKey(); + } + return null; + } + + /** + * 获取插件实例对象 + */ + public Object getInstance(String key) { + return dynamicJarLoaderService.getBeanInstance(key); + } +} diff --git a/src/main/java/com/cool/core/plugin/service/DynamicJarLoaderService.java b/src/main/java/com/cool/core/plugin/service/DynamicJarLoaderService.java new file mode 100644 index 0000000..11087e7 --- /dev/null +++ b/src/main/java/com/cool/core/plugin/service/DynamicJarLoaderService.java @@ -0,0 +1,171 @@ +package com.cool.core.plugin.service; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.cool.core.config.PluginJson; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.plugin.config.DynamicJarClassLoader; +import com.cool.core.util.AnnotationUtils; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import com.cool.modules.plugin.service.PluginInfoService; +import java.io.File; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 动态加载jar包 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DynamicJarLoaderService { + + final private PluginInfoService pluginInfoService; + + private final Map dynamicJarClassLoaderMap = new ConcurrentHashMap<>(); + + private final Map pluginMap = new ConcurrentHashMap<>(); + + public PluginJson install(String jarFilePath, boolean force) throws Exception { + URL jarUrl = new URL("jar:file:" + new File(jarFilePath).getAbsolutePath() + "!/"); + DynamicJarClassLoader dynamicJarClassLoader = new DynamicJarClassLoader(new URL[]{jarUrl}, + Thread.currentThread().getContextClassLoader()); + Thread.currentThread().setContextClassLoader(dynamicJarClassLoader); + InputStream inputStream = dynamicJarClassLoader.getResourceAsStream("plugin.json"); + CoolPreconditions.check(ObjUtil.isEmpty(inputStream), "不合规插件:未找到plugin.json文件"); + String pluginJsonStr = StrUtil.str(IoUtil.readBytes(inputStream), "UTF-8"); + PluginJson pluginJson = JSONUtil.toBean(pluginJsonStr, PluginJson.class); + CoolPreconditions.check(ObjUtil.isEmpty(pluginJson.getKey()), "该插件缺少唯一标识"); + if (ObjUtil.isNotEmpty(pluginJson.getHook())) { + // 查找hook是否已经存在,提示是否要替换,原hook将关闭 + PluginInfoEntity pluginInfoEntity = pluginInfoService + .getPluginInfoEntityByHookNoJarFile(pluginJson.getHook()); + if (!force) { + CoolPreconditions.returnData( + ObjUtil.isNotEmpty(pluginInfoEntity) + && !ObjUtil.equals(pluginInfoEntity.getKey(), pluginJson.getKey()), + new CoolPreconditions.ReturnData(1, + "插件已存在相同hook: {},继续安装将关闭原来插件(同名hook,只能有一个状态开启)", + pluginJson.getHook())); + + } else if (ObjUtil.isNotEmpty(pluginInfoEntity) + && !ObjUtil.equals(pluginInfoEntity.getKey(), pluginJson.getKey())) { + // 存在同名hook,需将原hook修改为关闭 + pluginJson.setSameHookId(pluginInfoEntity.getId()); + } + } + + // 加载类 + List> plugins = new ArrayList<>(); + try (JarFile jarFile = ((JarURLConnection) jarUrl.openConnection()).getJarFile()) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + if (jarEntry.getName().endsWith(".class")) { + String className = jarEntry.getName().replace('/', '.').substring(0, + jarEntry.getName().length() - 6); + try { + // 加载类 + Class clazz = dynamicJarClassLoader.loadClass(className); + if (AnnotationUtils.hasCoolPluginAnnotation(clazz)) { + plugins.add(clazz); + } + } catch (NoClassDefFoundError ignored) { + } + } + } + } + + // 校验插件 + checkPlugin(plugins); + registerPlugin(pluginJson.getKey(), plugins.get(0), dynamicJarClassLoader, force); + dynamicJarClassLoaderMap.put(pluginJson.getKey(), dynamicJarClassLoader); + + log.info("插件{}初始化成功.", pluginJson.getKey()); + return pluginJson; + } + + /** + * 注册插件 + */ + private void registerPlugin(String key, Class pluginClazz, + DynamicJarClassLoader dynamicJarClassLoader, + boolean force) { + if (!force && pluginMap.containsKey(key)) { + dynamicJarClassLoader.unload(); + CoolPreconditions.returnData( + new CoolPreconditions.ReturnData(1, "插件已存在,继续安装将覆盖")); + } + if (ObjUtil.isNotEmpty(key)) { + pluginMap.remove(key); + pluginMap.put(key, ReflectUtil.newInstance(pluginClazz)); + } + } + + /** + * 卸载 + */ + public boolean uninstall(String key) { + DynamicJarClassLoader dynamicJarClassLoader = getDynamicJarClassLoader(key); + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(dynamicJarClassLoader); + pluginMap.remove(key); + if (dynamicJarClassLoader != null) { + dynamicJarClassLoader.unload(); + } + } catch (Exception e) { + log.error("uninstall {}失败", key, e); + CoolPreconditions.alwaysThrow("卸载失败"); + } finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + log.info("卸载插件{}", key); + return true; + } + + /** + * 校验插件是否合法 + */ + private void checkPlugin(List> plugins) { + CoolPreconditions.checkEmpty(plugins, "未找到插件程序"); + int size = plugins.size(); + CoolPreconditions.check(size == 0, + "没找到符合规范的插件,插件需有@CoolPlugin注解,且以Plugin结尾(如:DemoPlugin)"); + CoolPreconditions.check(size > 1, + "识别到当前安装包有多个插件(只能有一个@CoolPlugin注解),一次只支持一个插件导入"); + } + + /** + * 获取插件实例对象 + */ + public Object getBeanInstance(String key) { + CoolPreconditions.checkEmpty(key, "插件key is null"); + if (pluginMap.containsKey(key)) { + return pluginMap.get(key); + } + CoolPreconditions.alwaysThrow("插件 {} 未找到", key); + return null; + } + + /** + * 获取自定义类加载器 + */ + public DynamicJarClassLoader getDynamicJarClassLoader(String key) { + return dynamicJarClassLoaderMap.get(key); + } +} diff --git a/src/main/java/com/cool/core/request/CrudOption.java b/src/main/java/com/cool/core/request/CrudOption.java new file mode 100644 index 0000000..fd18a2a --- /dev/null +++ b/src/main/java/com/cool/core/request/CrudOption.java @@ -0,0 +1,110 @@ +package com.cool.core.request; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.query.QueryColumn; +import com.mybatisflex.core.query.QueryCondition; +import com.mybatisflex.core.query.QueryWrapper; +import java.util.Arrays; +import lombok.Data; +import org.springframework.core.env.Environment; + +/** + * 查询构建器 + * + * @param + */ +@Data +public class CrudOption { + + private QueryWrapper queryWrapper; + private QueryColumn[] fieldEq; + private QueryColumn[] keyWordLikeFields; + private QueryColumn[] select; + private JSONObject requestParams; + + private Environment evn; + + public CrudOption(JSONObject requestParams) { + this.requestParams = requestParams; + this.queryWrapper = new QueryWrapper(); + this.evn = SpringUtil.getBean(Environment.class); + } + + public CrudOption fieldEq(QueryColumn... fields) { + this.fieldEq = fields; + return this; + } + + public QueryWrapper getQueryWrapper(Class entityClass) { + return build(this.queryWrapper, entityClass); + } + + public CrudOption queryWrapper(QueryWrapper queryWrapper) { + this.queryWrapper = queryWrapper; + return this; + } + + public CrudOption keyWordLikeFields(QueryColumn... fields) { + this.keyWordLikeFields = fields; + return this; + } + + public CrudOption select(QueryColumn... selects) { + this.select = selects; + return this; + } + + + /** + * 构建查询条件 + * + * @return QueryWrapper + */ + private QueryWrapper build(QueryWrapper queryWrapper, Class entityClass) { + if (ObjectUtil.isNotEmpty(fieldEq)) { + Arrays.stream(fieldEq).toList().forEach(filed -> queryWrapper.and( + filed.eq(requestParams.get(StrUtil.toCamelCase(filed.getName()))))); + } + Object keyWord = requestParams.get("keyWord"); + if (ObjectUtil.isNotEmpty(this.keyWordLikeFields) && ObjectUtil.isNotEmpty(keyWord)) { + QueryCondition queryCondition = new QueryCondition(); + Arrays.stream(keyWordLikeFields).toList().forEach(likeQueryColumn -> { + if (ObjectUtil.isEmpty(queryCondition.getColumn())) { + queryCondition.setColumn(likeQueryColumn); + queryCondition.setLogic(" LIKE "); + queryCondition.setValue("%" + keyWord + "%"); + } else { + queryCondition.or(likeQueryColumn.like(keyWord)); + } + }); + queryWrapper.and(queryCondition); + } + if (ObjectUtil.isNotEmpty(select)) { + queryWrapper.select(select); + } + // 排序 + order(queryWrapper, entityClass); + return queryWrapper; + } + + private void order(QueryWrapper queryWrapper, Class entityClass) { + Table tableAnnotation = AnnotationUtil.getAnnotation(entityClass, Table.class); + if (ObjectUtil.isEmpty(tableAnnotation)) { + // 该对象没有@Table注解,非Entity对象 + return; + } + String order = requestParams.getStr("order", + tableAnnotation.camelToUnderline() ? "create_time" : "createTime"); + String sort = requestParams.getStr("sort", "desc"); + if (StrUtil.isNotEmpty(order) && StrUtil.isNotEmpty(sort)) { + queryWrapper.orderBy( + tableAnnotation.camelToUnderline() ? StrUtil.toUnderlineCase(order) : order, + sort.equals("asc")); + } + } +} diff --git a/src/main/java/com/cool/core/request/R.java b/src/main/java/com/cool/core/request/R.java new file mode 100644 index 0000000..fe86175 --- /dev/null +++ b/src/main/java/com/cool/core/request/R.java @@ -0,0 +1,49 @@ +package com.cool.core.request; + +import java.util.HashMap; + +/** + * 返回信息 + */ +public class R extends HashMap { + private static final long serialVersionUID = 1L; + + public R() { + put("code", 1000); + put("message", "success"); + } + + public static R error() { + return error(1001, "请求方式不正确或服务出现异常"); + } + + public static R error(String msg) { + return error(1001, msg); + } + + public static R error(int code, String msg) { + R r = new R(); + r.put("code", code); + r.put("message", msg); + return r; + } + + public static R okMsg(String msg) { + R r = new R(); + r.put("message", msg); + return r; + } + + public static R ok() { + return new R(); + } + + public static R ok(Object data) { + return new R().put("data", data); + } + + public R put(String key, Object value) { + super.put(key, value); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/request/RequestParamsFilter.java b/src/main/java/com/cool/core/request/RequestParamsFilter.java new file mode 100644 index 0000000..70c14a6 --- /dev/null +++ b/src/main/java/com/cool/core/request/RequestParamsFilter.java @@ -0,0 +1,85 @@ +package com.cool.core.request; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.hutool.jwt.JWT; +import com.cool.core.util.BodyReaderHttpServletRequestWrapper; +import jakarta.servlet.*; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * 封装请求参数 URL参数 和 body JSON 到同一个 JSONObject 方便读取 + */ +@Component +@Order(2) +public class RequestParamsFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + Filter.super.init(filterConfig); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + // 防止流读取一次后就没有了, 所以需要将流继续写出去 + HttpServletRequest request = (HttpServletRequest) servletRequest; + JSONObject requestParams = new JSONObject(); + if (StrUtil.isNotEmpty(request.getContentType()) && request.getContentType().contains("multipart/form-data")) { + servletRequest.setAttribute("requestParams", requestParams); + filterChain.doFilter(servletRequest, servletResponse); + } else { + BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request); + String body = requestWrapper.getBodyString(requestWrapper); + if (StrUtil.isNotEmpty(body) && JSONUtil.isJson(body) && !JSONUtil.isJsonArray(body)) { + requestParams = JSONUtil.parseObj(body); + } + requestParams.set("body", body); + requestParams.putAll(getAllRequestParam(request)); + + Object jwtObj = request.getAttribute("tokenInfo"); + if (jwtObj != null) { + requestParams.set("tokenInfo", ((JWT) jwtObj).getPayload().getClaimsJson()); + } + requestWrapper.setAttribute("requestParams", requestParams); + + filterChain.doFilter(requestWrapper, servletResponse); + } + } + + /** + * 获取客户端请求参数中所有的信息 + * + * @param request + * @return + */ + private Map getAllRequestParam(final HttpServletRequest request) { + Map res = new HashMap<>(); + Enumeration temp = request.getParameterNames(); + if (null != temp) { + while (temp.hasMoreElements()) { + String en = (String) temp.nextElement(); + String value = request.getParameter(en); + res.put(en, value); + // 如果字段的值为空,判断若值为空,则删除这个字段> + if (null == res.get(en) || "".equals(res.get(en))) { + res.remove(en); + } + } + } + return res; + } + + @Override + public void destroy() { + Filter.super.destroy(); + } +} diff --git a/src/main/java/com/cool/core/request/RestInterceptor.java b/src/main/java/com/cool/core/request/RestInterceptor.java new file mode 100644 index 0000000..21a85cb --- /dev/null +++ b/src/main/java/com/cool/core/request/RestInterceptor.java @@ -0,0 +1,39 @@ +package com.cool.core.request; + +import com.cool.core.annotation.CoolRestController; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; + +/** + * 通用方法rest接口 + */ +@Component +public class RestInterceptor implements HandlerInterceptor { + private final static String[] rests = { "add", "delete", "update", "info", "list", "page" }; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 判断有无通用方法 + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + CoolRestController coolRestController = handlerMethod.getBeanType().getAnnotation(CoolRestController.class); + if (null != coolRestController) { + String[] urls = request.getRequestURI().split("/"); + String rest = urls[urls.length - 1]; + if (Arrays.asList(rests).contains(rest)) { + if (!Arrays.asList(coolRestController.api()).contains(rest)) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + return false; + } + } + } + } + return true; + } +} diff --git a/src/main/java/com/cool/core/request/prefix/AutoPrefixConfiguration.java b/src/main/java/com/cool/core/request/prefix/AutoPrefixConfiguration.java new file mode 100644 index 0000000..c699a80 --- /dev/null +++ b/src/main/java/com/cool/core/request/prefix/AutoPrefixConfiguration.java @@ -0,0 +1,17 @@ +package com.cool.core.request.prefix; + +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +/** + * 自定义路由规则 + */ +@Component +public class AutoPrefixConfiguration implements WebMvcRegistrations { + + @Override + public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { + return new AutoPrefixUrlMapping(); + } +} diff --git a/src/main/java/com/cool/core/request/prefix/AutoPrefixUrlMapping.java b/src/main/java/com/cool/core/request/prefix/AutoPrefixUrlMapping.java new file mode 100644 index 0000000..2d1b300 --- /dev/null +++ b/src/main/java/com/cool/core/request/prefix/AutoPrefixUrlMapping.java @@ -0,0 +1,100 @@ +package com.cool.core.request.prefix; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import com.cool.core.annotation.CoolRestController; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Set; + +/** + * 自动配置模块的路由 + */ +@Slf4j +public class AutoPrefixUrlMapping extends RequestMappingHandlerMapping { + + @Override + protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + CoolRestController[] annotations = handlerType.getAnnotationsByType(CoolRestController.class); + RequestMappingInfo info = super.getMappingForMethod(method, handlerType); + String packageName = handlerType.getPackage().getName(); + if (info != null && annotations.length > 0 && annotations[0].value().length == 0 + && packageName.contains("modules")) { + if (!checkApis(annotations, info)) { + return null; + } + String prefix = getPrefix(handlerType, packageName); + String cName = getCName(handlerType, prefix); + info = info.mutate().paths(prefix + "/" + cName).build().combine(info); + } + return info; + } + + /** + * 根据配置检查是否构建路由 + * + * @param annotations 注解 + * @param info 路由信息 + * @return 是否需要构建路由 + */ + private boolean checkApis(CoolRestController[] annotations, RequestMappingInfo info) { + String[] apis = new String[] { "add", "delete", "update", "page", "list", "info" }; + if (info.getPathPatternsCondition() == null) { + return true; + } + List setApis; + if (ArrayUtil.isNotEmpty(annotations)) { + CoolRestController coolRestController = annotations[0]; + setApis = CollUtil.toList(coolRestController.api()); + + Set methodPaths = info.getPathPatternsCondition().getPatternValues(); + String methodPath = methodPaths.iterator().next().replace("/", ""); + if (!CollUtil.toList(apis).contains(methodPath)) { + return true; + } else { + return setApis.contains(methodPath); + } + } + return false; + } + + /** + * 根据Controller名称构建路由地址 + * + * @param handlerType 类 + * @param prefix 路由前缀 + * @return url地址 + */ + private String getCName(Class handlerType, String prefix) { + String name = handlerType.getName(); + String[] names = name.split("[.]"); + name = names[names.length - 1]; + return name.toLowerCase().replace("controller", "").replace(prefix.replace("/", ""), ""); + } + + /** + * 构建路由前缀 + * + * @param handlerType 类 + * @param packageName 包名 + * @return 返回路由前缀 + */ + private String getPrefix(Class handlerType, String packageName) { + String dotPath = packageName.split("modules")[1]; // 将包路径中多于的部分截取掉 + String[] dotPaths = dotPath.replace(".controller", "").split("[.]"); + List paths = CollUtil.toList(dotPaths); + paths.removeIf(String::isEmpty); + // 第一和第二位互换位置 + String p0 = paths.get(0); + String p1 = paths.get(1); + paths.set(0, p1); + paths.set(1, p0); + dotPath = "/" + CollUtil.join(paths, "/"); + return dotPath; + } + +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/security/EntryPointUnauthorizedHandler.java b/src/main/java/com/cool/core/security/EntryPointUnauthorizedHandler.java new file mode 100644 index 0000000..1d86f8a --- /dev/null +++ b/src/main/java/com/cool/core/security/EntryPointUnauthorizedHandler.java @@ -0,0 +1,34 @@ +package com.cool.core.security; + +import cn.hutool.json.JSONUtil; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; + +/** + * 自定401返回值 + */ +@Component +public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + response.getWriter().write(JSONUtil.toJsonStr(new HashMap() { + { + put("code", "401"); + put("message", "未登录"); + } + })); + response.setStatus(401); + } + +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/security/IgnoredUrlsProperties.java b/src/main/java/com/cool/core/security/IgnoredUrlsProperties.java new file mode 100644 index 0000000..21cf905 --- /dev/null +++ b/src/main/java/com/cool/core/security/IgnoredUrlsProperties.java @@ -0,0 +1,19 @@ +package com.cool.core.security; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * 忽略token地址配置 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "ignored") +public class IgnoredUrlsProperties { + // 忽略权限列表 + private List urls = new ArrayList<>(); +} diff --git a/src/main/java/com/cool/core/security/JwtSecurityConfig.java b/src/main/java/com/cool/core/security/JwtSecurityConfig.java new file mode 100644 index 0000000..b5dbc68 --- /dev/null +++ b/src/main/java/com/cool/core/security/JwtSecurityConfig.java @@ -0,0 +1,88 @@ +package com.cool.core.security; + +import com.cool.modules.base.security.JwtAuthenticationTokenFilter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.util.DigestUtils; + +@EnableWebSecurity +@Configuration +@Slf4j +@RequiredArgsConstructor +public class JwtSecurityConfig { + + // 用户详情 + final private UserDetailsService userDetailsService; + + final private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; + // 401 + final private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler; + // 403 + final private RestAccessDeniedHandler restAccessDeniedHandler; + // 忽略权限控制的地址 + final private IgnoredUrlsProperties ignoredUrlsProperties; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + return httpSecurity + .authorizeHttpRequests( + conf -> conf.requestMatchers( + ignoredUrlsProperties.getUrls().toArray(String[]::new)) + .permitAll().anyRequest().authenticated()) + .headers(config -> config.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) + // 允许网页iframe + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationTokenFilter, + UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(config -> { + config.authenticationEntryPoint(entryPointUnauthorizedHandler); + config.accessDeniedHandler(restAccessDeniedHandler); + }).build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new PasswordEncoder() { + @Override + public String encode(CharSequence rawPassword) { + return DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes()); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return encodedPassword.equals( + DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes())); + } + }; + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) + throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/cool/core/security/MyAccessDecisionManager.java b/src/main/java/com/cool/core/security/MyAccessDecisionManager.java new file mode 100644 index 0000000..6542033 --- /dev/null +++ b/src/main/java/com/cool/core/security/MyAccessDecisionManager.java @@ -0,0 +1,50 @@ +package com.cool.core.security; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Iterator; + +/** + * 权限管理决断器 判断用户拥有的权限或角色是否有资源访问权限 + */ +@Slf4j +@Component +public class MyAccessDecisionManager implements AccessDecisionManager { + @Override + public void decide(Authentication authentication, Object o, Collection configAttributes) + throws AccessDeniedException, InsufficientAuthenticationException { + if (configAttributes == null) { + return; + } + Iterator iterator = configAttributes.iterator(); + while (iterator.hasNext()) { + ConfigAttribute c = iterator.next(); + String needPerm = c.getAttribute(); + for (GrantedAuthority ga : authentication.getAuthorities()) { + // 匹配用户拥有的ga 和 系统中的needPerm + if (needPerm.trim().equals(ga.getAuthority())) { + return; + } + } + } + throw new AccessDeniedException("抱歉,您没有访问权限"); + } + + @Override + public boolean supports(ConfigAttribute configAttribute) { + return true; + } + + @Override + public boolean supports(Class aClass) { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/security/MyFilterSecurityInterceptor.java b/src/main/java/com/cool/core/security/MyFilterSecurityInterceptor.java new file mode 100644 index 0000000..f22165b --- /dev/null +++ b/src/main/java/com/cool/core/security/MyFilterSecurityInterceptor.java @@ -0,0 +1,68 @@ +package com.cool.core.security; + +import jakarta.annotation.Resource; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.SecurityMetadataSource; +import org.springframework.security.access.intercept.AbstractSecurityInterceptor; +import org.springframework.security.access.intercept.InterceptorStatusToken; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; +import org.springframework.stereotype.Component; + +/** + * 权限管理拦截器 监控用户行为 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { + + final private FilterInvocationSecurityMetadataSource securityMetadataSource; + + @Resource + public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) { + super.setAccessDecisionManager(myAccessDecisionManager); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + FilterInvocation fi = new FilterInvocation(request, response, chain); + invoke(fi); + } + + public void invoke(FilterInvocation fi) throws IOException, ServletException { + InterceptorStatusToken token = super.beforeInvocation(fi); + try { + fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); + } finally { + super.afterInvocation(token, null); + } + } + + @Override + public void destroy() { + } + + @Override + public Class getSecureObjectClass() { + return FilterInvocation.class; + } + + @Override + public SecurityMetadataSource obtainSecurityMetadataSource() { + return this.securityMetadataSource; + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/security/RestAccessDeniedHandler.java b/src/main/java/com/cool/core/security/RestAccessDeniedHandler.java new file mode 100644 index 0000000..7954d05 --- /dev/null +++ b/src/main/java/com/cool/core/security/RestAccessDeniedHandler.java @@ -0,0 +1,34 @@ +package com.cool.core.security; + +import cn.hutool.json.JSONUtil; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; + +/** + * 自定403返回值 + */ +@Component +public class RestAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + response.getWriter().write(JSONUtil.toJsonStr(new HashMap() { + { + put("code", "403"); + put("message", "无权限"); + } + })); + response.setStatus(403); + } + +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/security/jwt/JwtTokenUtil.java b/src/main/java/com/cool/core/security/jwt/JwtTokenUtil.java new file mode 100644 index 0000000..127b0f7 --- /dev/null +++ b/src/main/java/com/cool/core/security/jwt/JwtTokenUtil.java @@ -0,0 +1,124 @@ +package com.cool.core.security.jwt; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTUtil; +import cn.hutool.jwt.JWTValidator; +import com.cool.core.config.CoolProperties; +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +import com.cool.modules.base.service.sys.BaseSysConfService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * JWT工具类 + */ +@Component +@RequiredArgsConstructor +public class JwtTokenUtil implements Serializable { + + final private CoolProperties coolProperties; + final private BaseSysConfService baseSysConfService; + final String key = "JWT_SECRET"; + + public long getExpire() { + return this.coolProperties.getToken().getExpire(); + } + + public long getRefreshExpire() { + return this.coolProperties.getToken().getRefreshExpire(); + } + + public String getSecret() { + String secret = baseSysConfService.getValueWithCache(key); + if (StrUtil.isBlank(secret)) { + secret = StrUtil.uuid().replaceAll("-", ""); + baseSysConfService.setValue(key, secret); + } + return secret; + } + + /** + * 生成令牌 + * + * @param tokenInfo 保存的用户信息 + * @return 令牌 + */ + public String generateToken(Map tokenInfo) { + tokenInfo.put("isRefresh", false); + Date expirationDate = new Date(System.currentTimeMillis() + getExpire() * 1000); + JWT jwt = JWT.create().setExpiresAt(expirationDate).setKey(getSecret().getBytes()) + .setPayload("created", new Date()); + tokenInfo.forEach(jwt::setPayload); + return jwt.sign(); + } + + /** + * 生成令牌 + * + * @param tokenInfo 保存的用户信息 + * @return 令牌 + */ + public String generateRefreshToken(Map tokenInfo) { + tokenInfo.put("isRefresh", true); + Date expirationDate = new Date(System.currentTimeMillis() + getRefreshExpire() * 1000); + JWT jwt = JWT.create().setExpiresAt(expirationDate).setKey(getSecret().getBytes()) + .setPayload("created", new Date()); + tokenInfo.forEach(jwt::setPayload); + return jwt.sign(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) { + JWT jwt = JWT.of(token); + return jwt.getPayload("username").toString(); + } + + /** + * 获得token信息 + * + * @param token 令牌 + * @return token信息 + */ + public JWT getTokenInfo(String token) { + return JWT.of(token); + } + + /** + * 判断令牌是否过期 + * + * @param token 令牌 + * @return 是否过期 + */ + public Boolean isTokenExpired(String token) { + try { + JWTValidator.of(token).validateDate(DateUtil.date()); + return false; + } catch (Exception e) { + return true; + } + } + + /** + * 验证令牌 + * + * @param token 令牌 + * @param username 用户 + * @return 是否有效 + */ + public Boolean validateToken(String token, String username) { + String tokenUsername = getUsernameFromToken(token); + String secret = getSecret(); + boolean isValidSignature = JWTUtil.verify(token, secret.getBytes()); + return (tokenUsername.equals(username) && !isTokenExpired(token) && isValidSignature); + } +} diff --git a/src/main/java/com/cool/core/security/jwt/JwtUser.java b/src/main/java/com/cool/core/security/jwt/JwtUser.java new file mode 100644 index 0000000..bddcfa7 --- /dev/null +++ b/src/main/java/com/cool/core/security/jwt/JwtUser.java @@ -0,0 +1,62 @@ +package com.cool.core.security.jwt; + +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +/** + * 用户信息 + */ +@Data +public class JwtUser implements UserDetails { + + public JwtUser(String username, String password, List perms, Boolean status) { + this.username = username; + this.password = password; + this.perms = perms; + this.status = status; + } + + private String username; + private String password; + private Boolean status; + private List perms; + + @Override + public Collection getAuthorities() { + return perms; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return status; + } +} diff --git a/src/main/java/com/cool/core/util/AnnotationUtils.java b/src/main/java/com/cool/core/util/AnnotationUtils.java new file mode 100644 index 0000000..2ab2c84 --- /dev/null +++ b/src/main/java/com/cool/core/util/AnnotationUtils.java @@ -0,0 +1,77 @@ +package com.cool.core.util; + +import com.cool.core.annotation.CoolPlugin; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Repository; +import org.springframework.stereotype.Service; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; + +@Slf4j +public class AnnotationUtils { + + /** + * 判断一个类是否有 Spring 核心注解 + * + * @param clazz 要检查的类 + * @return true 如果该类上添加了相应的 Spring 注解;否则返回 false + */ + public static boolean hasSpringAnnotation(Class clazz) { + if (clazz == null) { + return false; + } + // 是否是接口 + if (clazz.isInterface()) { + return false; + } + // 是否是抽象类 + if (Modifier.isAbstract(clazz.getModifiers())) { + return false; + } + + try { + if (clazz.getAnnotation(Component.class) != null || clazz.getAnnotation(Repository.class) != null + || clazz.getAnnotation(Service.class) != null || clazz.getAnnotation(Controller.class) != null + || clazz.getAnnotation(Configuration.class) != null) { + return true; + } + } catch (Exception e) { + log.error("出现异常:{}", e.getMessage()); + } + return false; + } + + /** + * 插件 + */ + public static boolean hasCoolPluginAnnotation(Class clazz) { + if (clazz == null) { + return false; + } + // 是否是接口 + if (clazz.isInterface()) { + return false; + } + // 是否是抽象类 + if (Modifier.isAbstract(clazz.getModifiers())) { + return false; + } + try { +// if (clazz.getAnnotation(CoolPlugin.class) != null) { +// return true; +// } + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + if (clazz.getAnnotation( + (Class) contextClassLoader.loadClass(CoolPlugin.class.getName())) != null) { + return true; + } + } catch (Exception e) { + log.error("出现异常:{}", e.getMessage()); + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/util/BodyReaderHttpServletRequestWrapper.java b/src/main/java/com/cool/core/util/BodyReaderHttpServletRequestWrapper.java new file mode 100644 index 0000000..2c783d2 --- /dev/null +++ b/src/main/java/com/cool/core/util/BodyReaderHttpServletRequestWrapper.java @@ -0,0 +1,119 @@ +package com.cool.core.util; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.charset.Charset; + +/** + * 保存流 + */ +@Slf4j +public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException { + super(request); + String sessionStream = getBodyString(request); + body = sessionStream.getBytes(Charset.forName("UTF-8")); + } + + /** + * 获取请求Body + * + * @param request + * @return + */ + public String getBodyString(final ServletRequest request) { + StringBuilder sb = new StringBuilder(); + InputStream inputStream = null; + BufferedReader reader = null; + try { + inputStream = cloneInputStream(request.getInputStream()); + reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); + String line = ""; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + log.error("err", e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + log.error("err", e); + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + log.error("err", e); + } + } + } + return sb.toString(); + } + + /** + * Description: 复制输入流
+ * + * @param inputStream + * @return
+ */ + public InputStream cloneInputStream(ServletInputStream inputStream) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + try { + while ((len = inputStream.read(buffer)) > -1) { + byteArrayOutputStream.write(buffer, 0, len); + } + byteArrayOutputStream.flush(); + } catch (IOException e) { + log.error("err", e); + } + InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + return byteArrayInputStream; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + + return new ServletInputStream() { + + @Override + public int read() throws IOException { + return bais.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } +} diff --git a/src/main/java/com/cool/core/util/CompilerUtils.java b/src/main/java/com/cool/core/util/CompilerUtils.java new file mode 100644 index 0000000..2f6f2d9 --- /dev/null +++ b/src/main/java/com/cool/core/util/CompilerUtils.java @@ -0,0 +1,86 @@ +package com.cool.core.util; + +import cn.hutool.core.io.FileUtil; +import com.mybatisflex.processor.MybatisFlexProcessor; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import javax.annotation.processing.Processor; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +public class CompilerUtils { + + /** + * 创建文件, 先删除在创建 + */ + public static void createFile(String content, String filePathStr) { + FileUtil.del(filePathStr); + File file = FileUtil.touch(filePathStr); + FileUtil.appendString(content, file, StandardCharsets.UTF_8.name()); + } + + public static void createMapper(String actModulePath, String fileName, String mapper) { + String pathStr = actModulePath + "/mapper/"; + String filePathStr = pathStr + fileName + "Mapper.java"; + createFile(mapper, filePathStr); + } + + public static void createServiceImpl(String actModulePath, String fileName, + String serviceImpl) { + String pathStr = actModulePath + "/service/impl/"; + String filePathStr = pathStr + fileName + "ServiceImpl.java"; + createFile(serviceImpl, filePathStr); + } + + public static void createService(String actModulePath, String fileName, String service) { + String pathStr = actModulePath + "/service/"; + String filePathStr = pathStr + fileName + "Service.java"; + createFile(service, filePathStr); + } + + public static String createEntity(String actModulePath, String fileName, String entity) { + String pathStr = actModulePath + "/entity/"; + String filePathStr = pathStr + fileName + "Entity.java"; + createFile(entity, filePathStr); + return filePathStr; + } + + public static void createController(String actModulePath, String fileName, String controller) { + String pathStr = actModulePath + "/controller/admin/"; + String filePathStr = pathStr + "Admin" + fileName + "Controller.java"; + createFile(controller, filePathStr); + } + + public static String createModule(String modulesPath, String module) { + String pathStr = modulesPath + "/" + module; + PathUtils.noExistsMk(pathStr); + return pathStr; + } + + public static void compilerEntityTableDef(String path) { +// JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); +// StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); +// +// Iterable compilationUnits = fileManager.getJavaFileObjects( +// path); +// +// // 设置注解处理器 +// Iterable processors = Arrays.asList(new MybatisFlexProcessor()); +// // 添加 -proc:only 选项 +// List options = Arrays.asList("-proc:only"); +// JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options, +// null, compilationUnits); +// task.setProcessors(processors); +// +// boolean success = task.call(); +// if (success) { +// System.out.println("Compilation and annotation processing completed successfully."); +// } else { +// System.out.println("Compilation and annotation processing failed."); +// } + } +} diff --git a/src/main/java/com/cool/core/util/ConvertUtil.java b/src/main/java/com/cool/core/util/ConvertUtil.java new file mode 100644 index 0000000..9836a19 --- /dev/null +++ b/src/main/java/com/cool/core/util/ConvertUtil.java @@ -0,0 +1,152 @@ +package com.cool.core.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import org.springframework.web.multipart.MultipartFile; + +/** + * 转换 + */ +public class ConvertUtil { + + /** + * 对象转数组 + * + * @param obj + * @return + */ + public static byte[] toByteArray(Object obj) { + byte[] bytes = null; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(obj); + oos.flush(); + bytes = bos.toByteArray(); + oos.close(); + bos.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + return bytes; + } + + /** + * 数组转对象 + * + * @param bytes + * @return + */ + public static Object toObject(byte[] bytes) { + Object obj = null; + try { + ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bis); + obj = ois.readObject(); + ois.close(); + bis.close(); + } catch (IOException | ClassNotFoundException ex) { + ex.printStackTrace(); + } + return obj; + } + + public static MultipartFile convertToMultipartFile(File file) { + FileInputStream inputStream = null; + try { + inputStream = new FileInputStream(file); + return new SimpleMultipartFile(file.getName(), inputStream); + } catch (FileNotFoundException e) { + e.printStackTrace(); + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + return null; + } + } + + // 简单的MultipartFile实现,用于模拟Spring中的MultipartFile对象 + static class SimpleMultipartFile implements MultipartFile { + + private String filename; + private InputStream inputStream; + + public SimpleMultipartFile(String filename, InputStream inputStream) { + this.filename = filename; + this.inputStream = inputStream; + } + + @Override + public String getName() { + return null; + } + + @Override + public String getOriginalFilename() { + return filename; + } + + @Override + public String getContentType() { + return "application/octet-stream"; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public long getSize() { + try { + return inputStream.available(); + } catch (IOException e) { + e.printStackTrace(); + return 0; + } + } + + @Override + public byte[] getBytes() throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, len); + } + return output.toByteArray(); + } + + @Override + public InputStream getInputStream() throws IOException { + return inputStream; + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException { + try (FileOutputStream outputStream = new FileOutputStream(dest)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, len); + } + } finally { + if (inputStream != null) { + inputStream.close(); + } + } + } + } +} diff --git a/src/main/java/com/cool/core/util/CoolPluginInvokers.java b/src/main/java/com/cool/core/util/CoolPluginInvokers.java new file mode 100644 index 0000000..6adfb88 --- /dev/null +++ b/src/main/java/com/cool/core/util/CoolPluginInvokers.java @@ -0,0 +1,165 @@ +package com.cool.core.util; + +import cn.hutool.json.JSONUtil; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.plugin.consts.PluginConsts; +import com.cool.core.plugin.service.DynamicJarLoaderService; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; + +/** + * 插件调用封装 + */ +@Slf4j +public class CoolPluginInvokers { + + private static final DynamicJarLoaderService dynamicJarLoaderService = SpringContextUtils + .getBean(DynamicJarLoaderService.class); + + /** + * 插件默认调用入口 + */ + public static Object invokePlugin(String key, String... params) { + return invoke(key, PluginConsts.invokePluginMethodName, params); + } + + /** + * 设置插件配置信息 + */ + public static void setPluginJson(String key, PluginInfoEntity entity) { + invoke(key, PluginConsts.setPluginJson, JSONUtil.toJsonStr(entity.getPluginJson())); + setApplicationContext(key); + } + + /** + * 设置 ApplicationContext 到插件类中 + */ + public static void setApplicationContext(String key) { + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread() + .setContextClassLoader(dynamicJarLoaderService.getDynamicJarClassLoader(key)); + Object beanInstance = dynamicJarLoaderService.getBeanInstance(key); + Method method = beanInstance.getClass().getSuperclass() + .getMethod(PluginConsts.setApplicationContext, + ApplicationContext.class); + method.invoke(beanInstance, SpringContextUtils.applicationContext); + } catch (Exception e) { + log.error("setApplicationContext err", e); + } finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + + /** + * 反射调用插件 + * + * @param key 插件key + * @param methodName 插件方法 + * @param params 参数 + */ + public static Object invoke(String key, String methodName, Object... params) { + Object beanInstance = dynamicJarLoaderService.getBeanInstance(key); + CoolPreconditions.checkEmpty(beanInstance, "未找到该插件:" + key); + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + try { + // 设置当前线程的上下文类加载器为插件的类加载器 + Thread.currentThread() + .setContextClassLoader(dynamicJarLoaderService.getDynamicJarClassLoader(key)); + log.info("调用插件类: {}, 方法: {} 参数: {}", key, methodName, params); + return invoke(beanInstance, methodName, params); + } catch (Exception e) { + log.error("调用插件{}.{}失败", key, methodName, e); + CoolPreconditions.alwaysThrow("调用插件{}.{}失败 {}", key, methodName, e.getMessage()); + } finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + return null; + } + + /** + * 反射调用插件 + * + * @param beanInstance 插件实例对象 + * @param methodName 插件方法 + * @param params 参数 + */ + private static Object invoke(Object beanInstance, String methodName, Object[] params) + throws InvocationTargetException, IllegalAccessException { + Class[] paramTypes = Arrays.stream(params).map(Object::getClass) + .toArray(Class[]::new); + Method method = findMethod(beanInstance.getClass(), methodName, paramTypes); + CoolPreconditions.check(method == null, "No such method: {} with parameters {}", methodName, + Arrays.toString(paramTypes)); + if (method.isVarArgs()) { + // 处理可变参数调用 + int varArgIndex = method.getParameterTypes().length - 1; + Object[] varArgs = (Object[]) java.lang.reflect.Array.newInstance( + method.getParameterTypes()[varArgIndex].getComponentType(), + params.length - varArgIndex); + System.arraycopy(params, varArgIndex, varArgs, 0, varArgs.length); + Object[] methodArgs = new Object[varArgIndex + 1]; + System.arraycopy(params, 0, methodArgs, 0, varArgIndex); + methodArgs[varArgIndex] = varArgs; + return method.invoke(beanInstance, methodArgs); + } else { + // 正常调用 + return method.invoke(beanInstance, params); + } + } + + // 查找方法,包括处理可变参数 + private static Method findMethod(Class clazz, String methodName, Class... paramTypes) { + try { + return clazz.getMethod(methodName, paramTypes); + } catch (NoSuchMethodException e) { + // Try to find a varargs method + for (Method method : clazz.getMethods()) { + if (method.getName().equals(methodName) && isAssignable(paramTypes, + method.getParameterTypes(), method.isVarArgs())) { + return method; + } + } + // If not found, try to find in superclass + if (clazz.getSuperclass() != null) { + return findMethod(clazz.getSuperclass(), methodName, paramTypes); + } + } + return null; + } + + private static boolean isAssignable(Class[] paramTypes, Class[] methodParamTypes, + boolean isVarArgs) { + if (isVarArgs) { + if (paramTypes.length < methodParamTypes.length - 1) { + return false; + } + for (int i = 0; i < methodParamTypes.length - 1; i++) { + if (!methodParamTypes[i].isAssignableFrom(paramTypes[i])) { + return false; + } + } + Class varArgType = methodParamTypes[methodParamTypes.length - 1].getComponentType(); + for (int i = methodParamTypes.length - 1; i < paramTypes.length; i++) { + if (!varArgType.isAssignableFrom(paramTypes[i])) { + return false; + } + } + return true; + } else { + if (paramTypes.length != methodParamTypes.length) { + return false; + } + for (int i = 0; i < paramTypes.length; i++) { + if (!methodParamTypes[i].isAssignableFrom(paramTypes[i])) { + return false; + } + } + return true; + } + } +} diff --git a/src/main/java/com/cool/core/util/EntityUtils.java b/src/main/java/com/cool/core/util/EntityUtils.java new file mode 100644 index 0000000..4bd47ef --- /dev/null +++ b/src/main/java/com/cool/core/util/EntityUtils.java @@ -0,0 +1,95 @@ +package com.cool.core.util; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.util.ObjUtil; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.query.QueryColumn; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +public class EntityUtils { + + private static Map> TABLE_MAP; + + public static Set findEntityClassName() { + Set entitySet = new HashSet<>(); + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = null; + try { + resources = resolver.getResources("classpath*:com/cool/modules/**/entity/**/*.class"); + for (Resource r : resources) { + String path = r.getURL().getPath(); + String className = path.substring(path.indexOf("com/cool/modules"), + path.lastIndexOf('.')).replace('/', '.'); + entitySet.add(className); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return entitySet; + } + + public static Map> findTableMap() { + if (ObjUtil.isEmpty(TABLE_MAP)) { + init(); + } + return TABLE_MAP; + } + + private static void init() { + Set classNames = EntityUtils.findEntityClassName(); + TABLE_MAP = new HashMap<>(); + classNames.forEach(className -> { + Class entityClass; + try { + entityClass = Class.forName(className); + Table tableAnnotation = AnnotationUtil.getAnnotation(entityClass, Table.class); + // key表名,value 实体对象 + TABLE_MAP.put(tableAnnotation.value(), entityClass); + } catch (Exception e) { + // do nothing + } + }); + } + + /** + * 获取实体类及其父类的字段名数组(排除指定字段) + * + * @return 字段名数组 + */ + public static QueryColumn[] getFieldNamesWithSuperClass(QueryColumn[] queryColumns, + String... excludeNames) { + return getFieldNamesListWithSuperClass(queryColumns, excludeNames).toArray( + new QueryColumn[0]); + } + + public static List getFieldNamesListWithSuperClass(QueryColumn[] queryColumns, + String... excludeNames) { + return Arrays.stream(queryColumns).toList().stream() + .filter(o -> ObjUtil.equals(o.getName(), excludeNames)).toList(); + } + + /** + * 检查字段名是否在排除列表中 + * + * @param fieldName 要检查的字段名 + * @param excludeNames 排除的字段名数组 + * @return 是否在排除列表中 + */ + private static boolean isExcluded(String fieldName, String[] excludeNames) { + for (String excludeName : excludeNames) { + if (fieldName.equals(excludeName)) { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/com/cool/core/util/IPUtils.java b/src/main/java/com/cool/core/util/IPUtils.java new file mode 100644 index 0000000..ed33d80 --- /dev/null +++ b/src/main/java/com/cool/core/util/IPUtils.java @@ -0,0 +1,46 @@ +package com.cool.core.util; + +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + + +/** + * IP地址 + */ +@Slf4j +@Component +public class IPUtils { + + /** + * 获取IP地址 + *

+ * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + */ + public String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + ip = request.getHeader("x-forwarded-for"); + if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StrUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + } catch (Exception e) { + log.error("IP extraction error", e); + } + return ip; + } +} diff --git a/src/main/java/com/cool/core/util/MapExtUtil.java b/src/main/java/com/cool/core/util/MapExtUtil.java new file mode 100644 index 0000000..3179ea2 --- /dev/null +++ b/src/main/java/com/cool/core/util/MapExtUtil.java @@ -0,0 +1,30 @@ +package com.cool.core.util; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; + +import java.util.Map; + +public class MapExtUtil extends MapUtil { + + /** + * 比较两个map key 和 value 是否一致 + */ + public static boolean compareMaps(Map map1, Map map2) { + if (ObjectUtil.isEmpty(map1) || ObjectUtil.isEmpty(map2)) { + return true; + } + if (map1.size() != map2.size()) { + return false; + } + for (Map.Entry entry : map1.entrySet()) { + if (!map2.containsKey(entry.getKey())) { + return false; + } + if (!ObjectUtil.equal(entry.getValue(), map2.get(entry.getKey()))) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/cool/core/util/PathUtils.java b/src/main/java/com/cool/core/util/PathUtils.java new file mode 100644 index 0000000..72bed3e --- /dev/null +++ b/src/main/java/com/cool/core/util/PathUtils.java @@ -0,0 +1,33 @@ +package com.cool.core.util; + +import cn.hutool.core.io.file.PathUtil; +import com.cool.CoolApplication; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class PathUtils { + + public static boolean isAbsolutePath(String pathStr) { + Path path = Paths.get(pathStr); + return path.isAbsolute(); + } + + public static String getUserDir() { + return System.getProperty("user.dir"); + } + + public static String getModulesPath() { + return getUserDir() + "/src/main/java/" + CoolApplication.class.getPackageName() + .replace(".", "/") + "/modules"; + } + + /** + * 路径不存在创建 + */ + public static void noExistsMk(String pathStr) { + Path path = Paths.get(pathStr); + if (PathUtil.exists(path, false)) { + PathUtil.mkParentDirs(path); + } + } +} diff --git a/src/main/java/com/cool/core/util/SpringContextUtils.java b/src/main/java/com/cool/core/util/SpringContextUtils.java new file mode 100644 index 0000000..f026531 --- /dev/null +++ b/src/main/java/com/cool/core/util/SpringContextUtils.java @@ -0,0 +1,45 @@ +package com.cool.core.util; + +import java.util.Map; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * Spring Context 工具类 + */ +@Component +public class SpringContextUtils implements ApplicationContextAware { + + public static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringContextUtils.applicationContext = applicationContext; + } + + public static Object getBean(String name) { + return applicationContext.getBean(name); + } + + public static T getBean(Class requiredType) { + return applicationContext.getBean(requiredType); + } + + public static boolean containsBean(String name) { + return applicationContext.containsBean(name); + } + + public static boolean isSingleton(String name) { + return applicationContext.isSingleton(name); + } + + public static Class getType(String name) { + return applicationContext.getType(name); + } + + public static Map getBeansOfType(Class requiredType) { + return applicationContext.getBeansOfType(requiredType); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/controller/admin/AdminBaseCommController.java b/src/main/java/com/cool/modules/base/controller/admin/AdminBaseCommController.java new file mode 100644 index 0000000..0fe42ff --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/admin/AdminBaseCommController.java @@ -0,0 +1,98 @@ +package com.cool.modules.base.controller.admin; + +import cn.hutool.core.lang.Dict; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.eps.CoolEps; +import com.cool.core.file.FileUploadStrategyFactory; +import com.cool.core.request.R; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import com.cool.modules.base.service.sys.BaseSysLoginService; +import com.cool.modules.base.service.sys.BaseSysPermsService; +import com.cool.modules.base.service.sys.BaseSysUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +/** + * 系统通用接口, 每个人都有权限操作 + */ +@RequiredArgsConstructor +@Tag(name = "系统通用", description = "系统通用") +@CoolRestController() +public class AdminBaseCommController { + + final private BaseSysPermsService baseSysPermsService; + + final private BaseSysUserService baseSysUserService; + + final private BaseSysLoginService baseSysLoginService; + + final private CoolEps coolEps; + + final private FileUploadStrategyFactory fileUploadStrategyFactory; + + @Operation(summary = "实体信息与路径", description = "系统所有的实体信息与路径,供前端自动生成代码与服务") + @GetMapping("/eps") + public R eps() { + return R.ok(coolEps.getAdmin()); + } + + @Operation(summary = "个人信息") + @GetMapping("/person") + public R person(@RequestAttribute() Long adminUserId) { + BaseSysUserEntity baseSysUserEntity = baseSysUserService.getById(adminUserId); + baseSysUserEntity.setPassword(null); + baseSysUserEntity.setPasswordV(null); + return R.ok(baseSysUserEntity); + } + + @Operation(summary = "修改个人信息") + @PostMapping("/personUpdate") + public R personUpdate(@RequestAttribute Long adminUserId, @RequestBody Dict body) { + baseSysUserService.personUpdate(adminUserId, body); + return R.ok(); + } + + @Operation(summary = "权限与菜单") + @GetMapping("/permmenu") + public R permmenu(@RequestAttribute() Long adminUserId) { + return R.ok(baseSysPermsService.permmenu(adminUserId)); + } + + @Operation(summary = "文件上传") + @PostMapping(value = "/upload", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, + MediaType.ALL_VALUE}) + public R upload( + @RequestPart(value = "file", required = false) @Parameter(description = "文件") MultipartFile[] files, + HttpServletRequest request) { + return R.ok(fileUploadStrategyFactory.upload(files, request)); + } + + @Operation(summary = "文件上传模式") + @GetMapping("/uploadMode") + public R uploadMode() { + return R.ok(fileUploadStrategyFactory.getMode()); + } + + @Operation(summary = "退出") + @PostMapping("/logout") + public R logout(@RequestAttribute Long adminUserId, @RequestAttribute String adminUsername) { + baseSysLoginService.logout(adminUserId, adminUsername); + return R.ok(); + } + + @Operation(summary = "编程") + @GetMapping("/program") + public R program() { + return R.ok("Java"); + } +} diff --git a/src/main/java/com/cool/modules/base/controller/admin/AdminBaseOpenController.java b/src/main/java/com/cool/modules/base/controller/admin/AdminBaseOpenController.java new file mode 100644 index 0000000..9f44739 --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/admin/AdminBaseOpenController.java @@ -0,0 +1,77 @@ +package com.cool.modules.base.controller.admin; + +import com.cool.core.annotation.CoolRestController; +import com.cool.core.eps.CoolEps; +import com.cool.core.file.FileUploadStrategyFactory; +import com.cool.core.request.R; +import com.cool.modules.base.dto.sys.BaseSysLoginDto; +import com.cool.modules.base.service.sys.BaseSysLoginService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 系统开放接口,无需权限校验 + */ +@RequiredArgsConstructor +@Tag(name = "系统开放", description = "系统开放") +@CoolRestController() +public class AdminBaseOpenController { + + final private BaseSysLoginService baseSysLoginService; + + final private CoolEps coolEps; + + final private FileUploadStrategyFactory fileUploadStrategyFactory; + + @Operation(summary = "实体信息与路径", description = "系统所有的实体信息与路径,供前端自动生成代码与服务") + @GetMapping("/eps") + public R eps() { + return R.ok(coolEps.getAdmin()); + } + + @Operation(summary = "获得网页内容的参数值") + @GetMapping("/html") + public R html() { + return R.ok(); + } + + @Operation(summary = "登录") + @PostMapping("/login") + public R login(@RequestBody BaseSysLoginDto baseSysLoginDto) { + return R.ok(baseSysLoginService.login(baseSysLoginDto)); + } + + @Operation(summary = "验证码") + @GetMapping("/captcha") + public R captcha(@Parameter(description = "类型:svg|base64") @RequestParam(defaultValue = "base64") String type, + @Parameter(description = "宽度") @RequestParam(defaultValue = "150") Integer width, + @Parameter(description = "高度") @RequestParam(defaultValue = "50") Integer height) { + return R.ok(baseSysLoginService.captcha(type, width, height)); + } + + @Operation(summary = "刷新token") + @GetMapping("/refreshToken") + public R refreshToken(String refreshToken) { + return R.ok(baseSysLoginService.refreshToken(refreshToken)); + } + + @Operation(summary = "文件上传") + @PostMapping(value = "/upload", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.ALL_VALUE }) + public R upload(@RequestPart(value = "file", required = false) @Parameter(description = "文件") MultipartFile[] files, + HttpServletRequest request) { + return R.ok(fileUploadStrategyFactory.upload(files, request)); + } + + @Operation(summary = "文件上传模式") + @GetMapping("/uploadMode") + public R uploadMode() { + return R.ok(fileUploadStrategyFactory.getMode()); + } +} diff --git a/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysDepartmentController.java b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysDepartmentController.java new file mode 100644 index 0000000..b969055 --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysDepartmentController.java @@ -0,0 +1,35 @@ +package com.cool.modules.base.controller.admin.sys; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity; +import com.cool.modules.base.service.sys.BaseSysDepartmentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * 系统部门 + */ +@Tag(name = "系统部门", description = "系统部门") +@CoolRestController(api = { "add", "delete", "update", "list" }) +public class AdminBaseSysDepartmentController + extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + } + + @Operation(summary = "排序") + @PostMapping("/order") + public R order(@RequestBody List list) { + this.service.order(list); + return R.ok(); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysLogController.java b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysLogController.java new file mode 100644 index 0000000..7b1ff35 --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysLogController.java @@ -0,0 +1,57 @@ +package com.cool.modules.base.controller.admin.sys; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.base.entity.sys.BaseSysLogEntity; +import com.cool.modules.base.entity.sys.table.BaseSysLogEntityTableDef; +import com.cool.modules.base.entity.sys.table.BaseSysUserEntityTableDef; +import com.cool.modules.base.service.sys.BaseSysConfService; +import com.cool.modules.base.service.sys.BaseSysLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; + +/** + * 系统日志 + */ +@RequiredArgsConstructor +@Tag(name = "系统日志", description = "系统日志") +@CoolRestController(api = {"page"}) +public class AdminBaseSysLogController extends BaseController { + + private final BaseSysConfService baseSysConfService; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption( + createOp() + .keyWordLikeFields( + BaseSysUserEntityTableDef.BASE_SYS_USER_ENTITY.NAME, + BaseSysLogEntityTableDef.BASE_SYS_LOG_ENTITY.PARAMS)); + } + + @Operation(summary = "清理日志") + @PostMapping("/clear") + public R clear() { + service.clear(true); + return R.ok(); + } + + @Operation(summary = "设置日志保存时间") + @PostMapping("/setKeep") + public R setKeep(@RequestAttribute JSONObject requestParams) { + baseSysConfService.updateValue("logKeep", requestParams.getStr("value")); + return R.ok(); + } + + @Operation(summary = "获得日志报错时间") + @PostMapping("/getKeep") + public R getKeep() { + return R.ok(baseSysConfService.getValue("logKeep")); + } +} diff --git a/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysMenuController.java b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysMenuController.java new file mode 100644 index 0000000..d1abeee --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysMenuController.java @@ -0,0 +1,56 @@ +package com.cool.modules.base.controller.admin.sys; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.request.R; +import com.cool.modules.base.entity.sys.BaseSysMenuEntity; +import com.cool.modules.base.service.sys.BaseSysMenuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * 系统菜单 + */ +@Tag(name = "系统菜单", description = "系统菜单") +@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}) +public class AdminBaseSysMenuController extends + BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + } + + @Operation(summary = "创建代码", description = "创建代码") + @PostMapping("/create") + public R create(@RequestBody() Map params) { + CoolPreconditions.checkEmpty(params.get("module"), "module参数不能为空"); + CoolPreconditions.checkEmpty(params.get("entity"), "entity参数不能为空"); + CoolPreconditions.checkEmpty(params.get("controller"), "controller参数不能为空"); + CoolPreconditions.checkEmpty(params.get("service"), "service参数不能为空"); + CoolPreconditions.checkEmpty(params.get("service-impl"), "service-impl参数不能为空"); + CoolPreconditions.checkEmpty(params.get("mapper"), "mapper参数不能为空"); + CoolPreconditions.checkEmpty(params.get("fileName"), "fileName参数不能为空"); + this.service.create(params); + return R.ok(); + } + + @Operation(summary = "导出", description = "导出") + @PostMapping("/export") + public R export(@RequestBody Map params) { + return R.ok(this.service.export(getIds(params))); + } + + @Operation(summary = "导入", description = "导入") + @PostMapping("/import") + public R importMenu(@RequestBody Map> params) { + CoolPreconditions.checkEmpty(params.get("menus"), "参数不能为空"); + return R.ok(this.service.importMenu(params.get("menus"))); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysParamController.java b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysParamController.java new file mode 100644 index 0000000..00fd851 --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysParamController.java @@ -0,0 +1,30 @@ +package com.cool.modules.base.controller.admin.sys; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.base.entity.sys.BaseSysParamEntity; +import com.cool.modules.base.service.sys.BaseSysParamService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 系统参数配置 + */ +@Tag(name = "系统参数配置", description = "系统参数配置") +@CoolRestController(api = { "add", "delete", "update", "page", "info" }) +public class AdminBaseSysParamController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + } + + @Operation(summary = "根据键返回网页的参数值") + @GetMapping("/html") + public String html(String key) { + return service.htmlByKey(key); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysRoleController.java b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysRoleController.java new file mode 100644 index 0000000..86d6b58 --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysRoleController.java @@ -0,0 +1,32 @@ +package com.cool.modules.base.controller.admin.sys; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.base.entity.sys.BaseSysRoleEntity; +import static com.cool.modules.base.entity.sys.table.BaseSysRoleEntityTableDef.BASE_SYS_ROLE_ENTITY; +import com.cool.modules.base.service.sys.BaseSysRoleService; +import com.mybatisflex.core.query.QueryWrapper; +import io.swagger.v3.oas.annotations.tags.Tag; + +import jakarta.servlet.http.HttpServletRequest; +/** + * 系统角色 + */ +@Tag(name = "系统角色", description = "系统角色") +@CoolRestController(api = { "add", "delete", "update", "page", "list", "info" }) +public class AdminBaseSysRoleController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + JSONObject tokenInfo = requestParams.getJSONObject("tokenInfo"); + boolean isAdmin = tokenInfo.getStr("username").equals("admin"); + + setPageOption(createOp().keyWordLikeFields(BASE_SYS_ROLE_ENTITY.NAME, BASE_SYS_ROLE_ENTITY.LABEL).queryWrapper(QueryWrapper.create().and(qw -> { + qw.eq(BASE_SYS_ROLE_ENTITY.USER_ID.getName(), tokenInfo.get("userId")).or(w -> { + w.in(BASE_SYS_ROLE_ENTITY.ID.getName(), tokenInfo.get("roleIds")); + }); + }, !isAdmin).and(BASE_SYS_ROLE_ENTITY.LABEL.ne("admin")))); + } + +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysUserController.java b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysUserController.java new file mode 100644 index 0000000..aa15e18 --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysUserController.java @@ -0,0 +1,34 @@ +package com.cool.modules.base.controller.admin.sys; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import com.cool.modules.base.service.sys.BaseSysUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 系统用户 + */ +@Tag(name = "系统用户", description = "系统用户") +@CoolRestController(api = { "add", "delete", "update", "page", "info" }) +public class AdminBaseSysUserController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + } + + @Operation(summary = "移动部门") + @PostMapping("/move") + public R move(@RequestAttribute JSONObject requestParams) { + service.move(requestParams.getLong("departmentId"), requestParams.get("userIds", Long[].class)); + return R.ok(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/controller/app/AppBaseCommController.java b/src/main/java/com/cool/modules/base/controller/app/AppBaseCommController.java new file mode 100644 index 0000000..447207f --- /dev/null +++ b/src/main/java/com/cool/modules/base/controller/app/AppBaseCommController.java @@ -0,0 +1,26 @@ +package com.cool.modules.base.controller.app; + +import com.cool.core.annotation.CoolRestController; +import com.cool.core.eps.CoolEps; +import com.cool.core.request.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * app通用接口 + */ +@RequiredArgsConstructor +@Tag(name = "应用通用", description = "应用通用") +@CoolRestController +public class AppBaseCommController { + + final private CoolEps coolEps; + + @Operation(summary = "实体信息与路径", description = "系统所有的实体信息与路径,供前端自动生成代码与服务") + @GetMapping("/eps") + public R eps() { + return R.ok(coolEps.getApp()); + } +} diff --git a/src/main/java/com/cool/modules/base/dto/sys/BaseSysLoginDto.java b/src/main/java/com/cool/modules/base/dto/sys/BaseSysLoginDto.java new file mode 100644 index 0000000..20f3149 --- /dev/null +++ b/src/main/java/com/cool/modules/base/dto/sys/BaseSysLoginDto.java @@ -0,0 +1,30 @@ +package com.cool.modules.base.dto.sys; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +/** + * 登录 + */ +@Data +@Schema(description = "登录参数") +public class BaseSysLoginDto { + + @Schema(description = "用户名") + @NotBlank + private String username; + + @Schema(description = "密码") + @NotBlank + private String password; + + @Schema(description = "验证码ID") + @NotBlank + private String captchaId; + + @Schema(description = "验证码") + @NotBlank + private String verifyCode; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysConfEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysConfEntity.java new file mode 100644 index 0000000..50343bf --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysConfEntity.java @@ -0,0 +1,25 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.tangzc.autotable.annotation.enums.IndexTypeEnum; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import com.tangzc.autotable.annotation.Index; +import lombok.Getter; +import lombok.Setter; + +/** + * 系统配置 + */ +@Getter +@Setter +@Table(value = "base_sys_conf", comment = "系统配置表") +public class BaseSysConfEntity extends BaseEntity { + @Index(type = IndexTypeEnum.UNIQUE) + @ColumnDefine(comment = "配置键", notNull = true) + private String cKey; + + @ColumnDefine(comment = "值", notNull = true, type = "text") + private String cValue; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysDepartmentEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysDepartmentEntity.java new file mode 100644 index 0000000..200a126 --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysDepartmentEntity.java @@ -0,0 +1,30 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +/** + * 系统部门 + */ +@Getter +@Setter +@Table(value = "base_sys_department", comment = "系统部门") +public class BaseSysDepartmentEntity extends BaseEntity { + @ColumnDefine(comment = "部门名称", notNull = true) + private String name; + + @ColumnDefine(comment = "上级部门ID", type = "bigint") + private Long parentId; + + @ColumnDefine(comment = "排序", defaultValue = "0") + private Integer orderNum; + + // 父菜单名称 + @Column(ignore = true) + private String parentName; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysLogEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysLogEntity.java new file mode 100644 index 0000000..32a2f4a --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysLogEntity.java @@ -0,0 +1,34 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.core.handler.Fastjson2TypeHandler; +import com.tangzc.autotable.annotation.Index; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "base_sys_log", comment = "系统日志表") +public class BaseSysLogEntity extends BaseEntity { + + @Index + @ColumnDefine(comment = "用户ID", type = "bigint") + private Long userId; + + @ColumnDefine(comment = "行为", length = 1000) + private String action; + + @ColumnDefine(comment = "IP", length = 50) + private String ip; + + @ColumnDefine(comment = "参数", type = "json") + @Column(typeHandler = Fastjson2TypeHandler.class) + private Object params; + + // 用户名称 + @Column(ignore = true) + private String name; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysMenuEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysMenuEntity.java new file mode 100644 index 0000000..3473135 --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysMenuEntity.java @@ -0,0 +1,54 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.tangzc.autotable.annotation.Index; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@Table(value = "base_sys_menu", comment = "系统菜单表") +public class BaseSysMenuEntity extends BaseEntity { + @Index + @ColumnDefine(comment = "父菜单ID", type = "bigint") + private Long parentId; + + @ColumnDefine(comment = "菜单名称") + private String name; + + @ColumnDefine(comment = "权限") + private String perms; + + @ColumnDefine(comment = "类型 0:目录 1:菜单 2:按钮", type = "tinyint", defaultValue = "0") + private Integer type; + + @ColumnDefine(comment = "图标") + private String icon; + + @ColumnDefine(comment = "排序", defaultValue = "0") + private Integer orderNum; + + @ColumnDefine(comment = "菜单地址") + private String router; + + @ColumnDefine(comment = "视图地址") + private String viewPath; + + @ColumnDefine(comment = "路由缓存", defaultValue = "true") + private Boolean keepAlive; + + @ColumnDefine(comment = "是否显示", defaultValue = "true") + private Boolean isShow; + + @Column(ignore = true) + private String parentName; + + @Column(ignore = true) + private List childMenus; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysParamEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysParamEntity.java new file mode 100644 index 0000000..97db9e7 --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysParamEntity.java @@ -0,0 +1,30 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.tangzc.autotable.annotation.Index; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "base_sys_param", comment = "系统参数配置") +public class BaseSysParamEntity extends BaseEntity { + @Index + @ColumnDefine(comment = "键", notNull = true) + private String keyName; + + @ColumnDefine(comment = "名称") + private String name; + + @ColumnDefine(comment = "数据", type = "text") + private String data; + + @ColumnDefine(comment = "数据类型 0:字符串 1:数组 2:键值对", defaultValue = "0", type = "tinyint") + private Integer dataType; + + @ColumnDefine(comment = "备注") + private String remark; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleDepartmentEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleDepartmentEntity.java new file mode 100644 index 0000000..f7a48ff --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleDepartmentEntity.java @@ -0,0 +1,20 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "base_sys_role_department", comment = "系统角色部门") +public class BaseSysRoleDepartmentEntity extends BaseEntity { + + @ColumnDefine(comment = "角色ID", type = "bigint") + private Long roleId; + + @ColumnDefine(comment = "部门ID", type = "bigint") + private Long departmentId; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleEntity.java new file mode 100644 index 0000000..2623fce --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleEntity.java @@ -0,0 +1,43 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.handler.Fastjson2TypeHandler; +import com.tangzc.autotable.annotation.Index; +import com.tangzc.autotable.annotation.enums.IndexTypeEnum; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "base_sys_role", comment = "系统角色表") +public class BaseSysRoleEntity extends BaseEntity { + + @Index + @ColumnDefine(comment = "用户ID", notNull = true, type = "bigint") + private Long userId; + + @ColumnDefine(comment = "名称", notNull = true) + private String name; + + @Index(type = IndexTypeEnum.UNIQUE) + @ColumnDefine(comment = "角色标签", notNull = true) + private String label; + + @ColumnDefine(comment = "备注") + private String remark; + + @ColumnDefine(comment = "数据权限是否关联上下级", defaultValue = "1") + private Integer relevance; + + @ColumnDefine(comment = "菜单权限", type = "json") + @Column(typeHandler = Fastjson2TypeHandler.class) + private List menuIdList; + + @ColumnDefine(comment = "部门权限", type = "json") + @Column(typeHandler = Fastjson2TypeHandler.class) + private List departmentIdList; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleMenuEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleMenuEntity.java new file mode 100644 index 0000000..0886904 --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleMenuEntity.java @@ -0,0 +1,19 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "base_sys_role_menu", comment = "系统角色菜单表") +public class BaseSysRoleMenuEntity extends BaseEntity { + @ColumnDefine(comment = "菜单", type = "bigint") + private Long menuId; + + @ColumnDefine(comment = "角色ID", type = "bigint") + private Long roleId; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserEntity.java new file mode 100644 index 0000000..8098af9 --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserEntity.java @@ -0,0 +1,63 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.tangzc.autotable.annotation.Index; +import com.tangzc.autotable.annotation.enums.IndexTypeEnum; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import com.tangzc.autotable.annotation.Index; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "base_sys_user", comment = "系统用户表") +public class BaseSysUserEntity extends BaseEntity { + @Index + @ColumnDefine(comment = "部门ID", type = "bigint") + private Long departmentId; + + @ColumnDefine(comment = "姓名") + private String name; + + @Index(type = IndexTypeEnum.UNIQUE) + @ColumnDefine(comment = "用户名", length = 100, notNull = true) + private String username; + + @ColumnDefine(comment = "密码", notNull = true) + private String password; + + @ColumnDefine(comment = "密码版本", defaultValue = "1") + private Integer passwordV; + + @ColumnDefine(comment = "昵称", notNull = true) + private String nickName; + + @ColumnDefine(comment = "头像") + private String headImg; + + @ColumnDefine(comment = "手机号") + private String phone; + + @ColumnDefine(comment = "邮箱") + private String email; + + @ColumnDefine(comment = "备注") + private String remark; + + @ColumnDefine(comment = "状态 0:禁用 1:启用", defaultValue = "1", type = "tinyint") + private Integer status; + + // 部门名称 + @Column(ignore = true) + private String departmentName; + + // 角色名称 + @Column(ignore = true) + private String roleName; + + @ColumnDefine(comment = "socketId") + private String socketId; +} diff --git a/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserRoleEntity.java b/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserRoleEntity.java new file mode 100644 index 0000000..17dbbc2 --- /dev/null +++ b/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserRoleEntity.java @@ -0,0 +1,19 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "base_sys_user_role", comment = "系统用户角色表") +public class BaseSysUserRoleEntity extends BaseEntity { + @ColumnDefine(comment = "用户ID", type = "bigint") + private Long userId; + + @ColumnDefine(comment = "角色ID", type = "bigint") + private Long roleId; +} diff --git a/src/main/java/com/cool/modules/base/filter/BaseLogFilter.java b/src/main/java/com/cool/modules/base/filter/BaseLogFilter.java new file mode 100644 index 0000000..9e10474 --- /dev/null +++ b/src/main/java/com/cool/modules/base/filter/BaseLogFilter.java @@ -0,0 +1,33 @@ +package com.cool.modules.base.filter; + +import cn.hutool.json.JSONObject; +import com.cool.modules.base.service.sys.BaseSysLogService; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(10) +@RequiredArgsConstructor +public class BaseLogFilter implements Filter { + + final private BaseSysLogService baseSysLogService; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) + throws IOException, ServletException { + // 记录日志 + HttpServletRequest request = (HttpServletRequest) servletRequest; + baseSysLogService.record(request, (JSONObject) request.getAttribute("requestParams")); + filterChain.doFilter(servletRequest, servletResponse); + } + +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysConfMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysConfMapper.java new file mode 100644 index 0000000..298448c --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysConfMapper.java @@ -0,0 +1,11 @@ +package com.cool.modules.base.mapper.sys; + +import com.cool.modules.base.entity.sys.BaseSysConfEntity; +import com.mybatisflex.core.BaseMapper; + +/** + * 系统配置 + */ +public interface BaseSysConfMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysDepartmentMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysDepartmentMapper.java new file mode 100644 index 0000000..cc3148e --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysDepartmentMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.base.mapper.sys; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity; + +/** + * 系统部门 + */ +public interface BaseSysDepartmentMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysLogMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysLogMapper.java new file mode 100644 index 0000000..5bd6f09 --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysLogMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.base.mapper.sys; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.base.entity.sys.BaseSysLogEntity; + +/** + * 系统日志 + */ +public interface BaseSysLogMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysMenuMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysMenuMapper.java new file mode 100644 index 0000000..ab39f68 --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysMenuMapper.java @@ -0,0 +1,20 @@ +package com.cool.modules.base.mapper.sys; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.base.entity.sys.BaseSysMenuEntity; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 系统菜单 + */ +public interface BaseSysMenuMapper extends BaseMapper { + /** + * 根据角色ID获得所有菜单 + * + * @param roleIds 角色ID集合 + * @return SysMenuEntity + */ + List getMenus(@Param("roleIds") Long[] roleIds); +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysParamMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysParamMapper.java new file mode 100644 index 0000000..49ce1bd --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysParamMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.base.mapper.sys; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.base.entity.sys.BaseSysParamEntity; + +/** + * 系统参数配置 + */ +public interface BaseSysParamMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleDepartmentMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleDepartmentMapper.java new file mode 100644 index 0000000..9f3f027 --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleDepartmentMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.base.mapper.sys; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.base.entity.sys.BaseSysRoleDepartmentEntity; + +/** + * 系统角色部门 + */ +public interface BaseSysRoleDepartmentMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMapper.java new file mode 100644 index 0000000..6264504 --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.base.mapper.sys; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.base.entity.sys.BaseSysRoleEntity; + +/** + * 系统角色 + */ +public interface BaseSysRoleMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMenuMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMenuMapper.java new file mode 100644 index 0000000..4d488f0 --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMenuMapper.java @@ -0,0 +1,18 @@ +package com.cool.modules.base.mapper.sys; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.base.entity.sys.BaseSysRoleMenuEntity; +import org.apache.ibatis.annotations.Param; + +/** + * 系统角色菜单 + */ +public interface BaseSysRoleMenuMapper extends BaseMapper { + /** + * 跟菜单关联的所有用户 + * + * @param menuId 菜单 + * @return 所有用户ID + */ + Long[] userIds(@Param("menuId") Long menuId); +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserMapper.java new file mode 100644 index 0000000..37f4c90 --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserMapper.java @@ -0,0 +1,11 @@ +package com.cool.modules.base.mapper.sys; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; + +/** + * 系统用户 + */ +public interface BaseSysUserMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserRoleMapper.java b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserRoleMapper.java new file mode 100644 index 0000000..51b8105 --- /dev/null +++ b/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserRoleMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.base.mapper.sys; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.base.entity.sys.BaseSysUserRoleEntity; + +/** + * 系统用户角色 + */ +public interface BaseSysUserRoleMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/base/security/CoolSecurityUtil.java b/src/main/java/com/cool/modules/base/security/CoolSecurityUtil.java new file mode 100644 index 0000000..8997937 --- /dev/null +++ b/src/main/java/com/cool/modules/base/security/CoolSecurityUtil.java @@ -0,0 +1,63 @@ +package com.cool.modules.base.security; + +import cn.hutool.json.JSONObject; +import com.cool.core.cache.CoolCache; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +/** + * Security 工具类 + */ +@Component +@RequiredArgsConstructor +public class CoolSecurityUtil { + + final private CoolCache coolCache; + + /** + * 登录的用户名 + * + * @return 用户名 + */ + public String username() { + return SecurityContextHolder.getContext().getAuthentication().getName(); + } + + /** + * 获得jwt中的信息 + * + * @param requestParams 请求参数 + * @return jwt + */ + public JSONObject userInfo(JSONObject requestParams) { + JSONObject tokenInfo = requestParams.getJSONObject("tokenInfo"); + if (tokenInfo != null) { + tokenInfo.set("department", + coolCache.get("admin:department:" + tokenInfo.get("userId"))); + tokenInfo.set("roleIds", coolCache.get("admin:roleIds:" + tokenInfo.get("userId"))); + } + return tokenInfo; + } + + /** + * 退出登录 + * + * @param adminUserId 用户ID + * @param username 用户名 + */ + public void logout(Long adminUserId, String username) { + coolCache.del("admin:department:" + adminUserId, "admin:passwordVersion:" + adminUserId, + "admin:userInfo:" + adminUserId, "admin:userDetails:" + username); + } + + /** + * 退出登录 + * + * @param userEntity 用户 + */ + public void logout(BaseSysUserEntity userEntity) { + logout(userEntity.getId(), userEntity.getUsername()); + } +} diff --git a/src/main/java/com/cool/modules/base/security/JwtAuthenticationTokenFilter.java b/src/main/java/com/cool/modules/base/security/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..11b08ca --- /dev/null +++ b/src/main/java/com/cool/modules/base/security/JwtAuthenticationTokenFilter.java @@ -0,0 +1,65 @@ +package com.cool.modules.base.security; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWT; +import com.cool.core.cache.CoolCache; +import com.cool.core.security.jwt.JwtTokenUtil; +import com.cool.core.security.jwt.JwtUser; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Token过滤器 + */ +@Order(1) +@Component +@RequiredArgsConstructor +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { + + final private JwtTokenUtil jwtTokenUtil; + final private CoolCache coolCache; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain chain) + throws ServletException, IOException { + String authToken = request.getHeader("Authorization"); + if (!StrUtil.isEmpty(authToken)) { + JWT jwt = jwtTokenUtil.getTokenInfo(authToken); + String username = jwt.getPayload("username").toString(); + if (username != null + && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = coolCache.get("admin:userDetails:" + username, + JwtUser.class); + Integer passwordV = Convert.toInt(jwt.getPayload("passwordVersion")); + Integer rv = coolCache.get("admin:passwordVersion:" + jwt.getPayload("userId"), + Integer.class); + if (jwtTokenUtil.validateToken(authToken, username) && Objects.equals(passwordV, rv) + && userDetails != null) { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + request.setAttribute("adminUsername", jwt.getPayload("username")); + request.setAttribute("adminUserId", jwt.getPayload("userId")); + request.setAttribute("tokenInfo", jwt); + } + } + } + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/cool/modules/base/security/JwtUserDetailsServiceImpl.java b/src/main/java/com/cool/modules/base/security/JwtUserDetailsServiceImpl.java new file mode 100644 index 0000000..99e5853 --- /dev/null +++ b/src/main/java/com/cool/modules/base/security/JwtUserDetailsServiceImpl.java @@ -0,0 +1,57 @@ +package com.cool.modules.base.security; + +import cn.hutool.core.util.ObjectUtil; +import com.cool.core.cache.CoolCache; +import com.cool.core.security.jwt.JwtUser; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import com.cool.modules.base.service.sys.BaseSysPermsService; +import com.cool.modules.base.service.sys.BaseSysUserService; +import com.mybatisflex.core.query.QueryWrapper; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +/** + * 获得用户信息 + */ +@Component +@RequiredArgsConstructor +public class JwtUserDetailsServiceImpl implements UserDetailsService { + + final private BaseSysUserService baseSysUserService; + final private BaseSysPermsService baseSysPermsService; + final private CoolCache coolCache; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + BaseSysUserEntity sysUserEntity = baseSysUserService.getMapper().selectOneByQuery( + QueryWrapper.create().eq(BaseSysUserEntity::getUsername, username) + .eq(BaseSysUserEntity::getStatus, 1)); + if (ObjectUtil.isEmpty(sysUserEntity)) { + throw new UsernameNotFoundException("用户名不存在"); + } + List authority = new ArrayList<>(); + String[] perms = baseSysPermsService.getPerms(sysUserEntity.getId()); + for (String perm : perms) { + authority.add(new SimpleGrantedAuthority(perm)); + } + Long[] departmentIds = baseSysPermsService.getDepartmentIdsByRoleIds(sysUserEntity.getId()); + JwtUser jwtUser = new JwtUser(sysUserEntity.getUsername(), sysUserEntity.getPassword(), + authority, + sysUserEntity.getStatus() == 1); + Long[] roleIds = baseSysPermsService.getRoles(sysUserEntity); + coolCache.set("admin:userDetails:" + jwtUser.getUsername(), jwtUser); + coolCache.set("admin:passwordVersion:" + sysUserEntity.getId(), + sysUserEntity.getPasswordV()); + coolCache.set("admin:userInfo:" + sysUserEntity.getId(), sysUserEntity); + coolCache.set("admin:department:" + sysUserEntity.getId(), departmentIds); + coolCache.set("admin:roleIds:" + sysUserEntity.getId(), roleIds); + return jwtUser; + } +} diff --git a/src/main/java/com/cool/modules/base/security/MySecurityMetadataSource.java b/src/main/java/com/cool/modules/base/security/MySecurityMetadataSource.java new file mode 100644 index 0000000..7322d1d --- /dev/null +++ b/src/main/java/com/cool/modules/base/security/MySecurityMetadataSource.java @@ -0,0 +1,74 @@ +package com.cool.modules.base.security; + +import com.cool.modules.base.service.sys.BaseSysPermsService; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; +import org.springframework.stereotype.Component; + +/** + * 权限资源管理器 为权限决断器提供支持 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource { + + final private BaseSysPermsService baseSysPermsService; + + private Map> map = null; + + /** + * 加载权限表中所有操作请求权限 + */ + public void loadResourceDefine() { + map = new HashMap<>(); + Collection configAttributes; + ConfigAttribute cfg; + String[] perms = baseSysPermsService.getAllPerms(); + // 获取启用的权限操作请求 + for (String perm : perms) { + configAttributes = new ArrayList<>(); + cfg = new SecurityConfig(perm); + // 作为MyAccessDecisionManager类的decide的第三个参数 + configAttributes.add(cfg); + // 用权限的path作为map的key,用ConfigAttribute的集合作为value + map.put(perm.replaceAll(":", "/"), configAttributes); + } + } + + /** + * 判定用户请求的url是否在权限表中 如果在权限表中,则返回给decide方法,用来判定用户是否有此权限 如果不在权限表中则放行 + * + * @param o + * @return + * @throws IllegalArgumentException + */ + @Override + public Collection getAttributes(Object o) throws IllegalArgumentException { + if (map == null) { + loadResourceDefine(); + } + // Object中包含用户请求request + String url = ((FilterInvocation) o).getRequestUrl(); + return map.get(url.replace("/admin/", "").split("[?]")[0]); + + } + + @Override + public Collection getAllConfigAttributes() { + return new ArrayList<>(); + } + + @Override + public boolean supports(Class aClass) { + return true; + } +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysConfService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysConfService.java new file mode 100644 index 0000000..3117a6f --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysConfService.java @@ -0,0 +1,41 @@ +package com.cool.modules.base.service.sys; + +import com.cool.core.base.BaseService; +import com.cool.modules.base.entity.sys.BaseSysConfEntity; + +/** + * 系统配置 + */ +public interface BaseSysConfService extends BaseService { + /** + * 更新配置 + * + * @param key 键 + * @param value 值 + */ + void updateValue(String key, String value); + + /** + * 获得值 + * + * @param key 键 + * @return 值 + */ + String getValue(String key); + + /** + * 获得值(带缓存) + * + * @param key 键 + * @return 值 + */ + String getValueWithCache(String key); + + /** + * 设置值 + * + * @param key 键 + * @param value 值 + */ + void setValue(String key, String value); +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysDepartmentService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysDepartmentService.java new file mode 100644 index 0000000..502862d --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysDepartmentService.java @@ -0,0 +1,18 @@ +package com.cool.modules.base.service.sys; + +import com.cool.core.base.BaseService; +import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity; + +import java.util.List; + +/** + * 系统部门 + */ +public interface BaseSysDepartmentService extends BaseService { + /** + * 排序 + * + * @param list 新的排序 + */ + void order(List list); +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysLogService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysLogService.java new file mode 100644 index 0000000..4ca596d --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysLogService.java @@ -0,0 +1,27 @@ +package com.cool.modules.base.service.sys; + +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseService; +import com.cool.modules.base.entity.sys.BaseSysLogEntity; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 系统日志 + */ +public interface BaseSysLogService extends BaseService { + /** + * 清理日志 + * + * @param isAll 是否全部清除 + */ + void clear(boolean isAll); + + /** + * 日志记录 + * + * @param requestParams 请求参数 + * @param request 请求 + */ + void record(HttpServletRequest request, JSONObject requestParams); +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysLoginService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysLoginService.java new file mode 100644 index 0000000..731e45f --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysLoginService.java @@ -0,0 +1,42 @@ +package com.cool.modules.base.service.sys; + +import com.cool.modules.base.dto.sys.BaseSysLoginDto; + +/** + * 系统登录 + */ +public interface BaseSysLoginService { + /** + * 验证码 + * + * @param type 类型 svg base64 svg是node版本的, java版本用base64, svg未实现 + * @param width 宽度 + * @param height 高度 + * @return base64 验证码与ID + */ + Object captcha(String type, Integer width, Integer height); + + /** + * 登录 + * + * @param baseSysLoginDto 登录必要信息 + * @return token与相关的过期信息 + */ + Object login(BaseSysLoginDto baseSysLoginDto); + + /** + * 退出登录 + * + * @param adminUserId 用户ID + * @param username 用户名称 + */ + void logout(Long adminUserId, String username); + + /** + * 刷新token + * + * @param refreshToken 刷新token + * @return 新的token + */ + Object refreshToken(String refreshToken); +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysMenuService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysMenuService.java new file mode 100644 index 0000000..e7753d9 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysMenuService.java @@ -0,0 +1,18 @@ +package com.cool.modules.base.service.sys; + +import com.cool.core.base.BaseService; +import com.cool.modules.base.entity.sys.BaseSysMenuEntity; +import java.util.List; +import java.util.Map; + +/** + * 系统菜单 + */ +public interface BaseSysMenuService extends BaseService { + + Object export(List ids); + + boolean importMenu(List menus); + + void create(Map params); +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysParamService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysParamService.java new file mode 100644 index 0000000..7926140 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysParamService.java @@ -0,0 +1,25 @@ +package com.cool.modules.base.service.sys; + +import com.cool.core.base.BaseService; +import com.cool.modules.base.entity.sys.BaseSysParamEntity; + +/** + * 系统参数配置 + */ +public interface BaseSysParamService extends BaseService { + /** + * 根据key获得网页内容 + * + * @param key 键 + * @return 网页内容 + */ + String htmlByKey(String key); + + /** + * 根据key获得数据 + * + * @param key 键 + * @return 数据 + */ + String dataByKey(String key); +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysPermsService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysPermsService.java new file mode 100644 index 0000000..2333b66 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysPermsService.java @@ -0,0 +1,160 @@ +package com.cool.modules.base.service.sys; + +import com.cool.modules.base.entity.sys.BaseSysMenuEntity; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; + +import java.util.List; + +/** + * 权限菜单 + */ +public interface BaseSysPermsService { + /** + * 获得权限缓存 + * + * @param userId 用户ID + * @return 返回用户相关的权限信息 + */ + String[] getPermsCache(Long userId); + + /** + * 获得权限 + * + * @param userId 用户ID + * @return 返回用户相关的权限信息 + */ + String[] getPerms(Long userId); + + /** + * 获得权限 + * + * @param roleIds 用户角色数组 + * @return 返回用户相关的权限信息 + */ + String[] getPerms(Long[] roleIds); + + /** + * 获得菜单 + * + * @param roleIds 角色 + * @return 返回菜单 + */ + List getMenus(Long[] roleIds); + + /** + * 获得菜单 + * + * @param userId 用户ID + * @return 返回菜单 + */ + List getMenus(Long userId); + + /** + * 获得菜单 + * + * @param username 用户名 + * @return 返回菜单 + */ + List getMenus(String username); + + /** + * 获得角色数组 + * + * @param userId 用户ID + * @return 返回角色数组 + */ + Long[] getRoles(Long userId); + + /** + * 获得角色数组 + * + * @param username 用户名 + * @return 返回角色数组 + */ + Long[] getRoles(String username); + + /** + * 获得登录用户的部门权限 + * + * @return 部门ID集合 + */ + Long[] loginDepartmentIds(); + + /** + * 根据角色获得部门ID + * + * @param roleIds 角色ID数组 + * @return 部门ID数组 + */ + Long[] getDepartmentIdsByRoleIds(Long[] roleIds); + + /** + * 根据用户ID获得部门ID + * + * @param userId 角色ID数组 + * @return 部门ID数组 + */ + Long[] getDepartmentIdsByRoleIds(Long userId); + + /** + * 获得角色数组 + * + * @param userEntity 用户 + * @return 返回角色数组 + */ + Long[] getRoles(BaseSysUserEntity userEntity); + + /** + * 所有的操作权限 + * + * @return 返回所有的操作权限 + */ + String[] getAllPerms(); + + /** + * 用户的权限菜单 + * + * @param adminUserId 登录的用户 + * @return 权限菜单 + */ + Object permmenu(Long adminUserId); + + /** + * 更新角色权限 + * + * @param roleId 角色ID + * @param menuIdList 菜单ID + * @param departmentIds 部门ID + */ + void updatePerms(Long roleId, Long[] menuIdList, Long[] departmentIds); + + /** + * 更新用户角色 + * + * @param userId 用户ID + * @param roleIdList 角色集合 + */ + void updateUserRole(Long userId, Long[] roleIdList); + + /** + * 刷新权限 + * + * @param userId 用户ID + */ + void refreshPerms(Long userId); + + /** + * 刷新权限 + * + * @param menuId 用户ID + */ + void refreshPermsByMenuId(Long menuId); + + /** + * 刷新权限 + * + * @param roleId 角色ID + */ + void refreshPermsByRoleId(Long roleId); + +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysRoleService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysRoleService.java new file mode 100644 index 0000000..f9e4546 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysRoleService.java @@ -0,0 +1,10 @@ +package com.cool.modules.base.service.sys; + +import com.cool.core.base.BaseService; +import com.cool.modules.base.entity.sys.BaseSysRoleEntity; + +/** + * 系统角色 + */ +public interface BaseSysRoleService extends BaseService { +} diff --git a/src/main/java/com/cool/modules/base/service/sys/BaseSysUserService.java b/src/main/java/com/cool/modules/base/service/sys/BaseSysUserService.java new file mode 100644 index 0000000..0a6197a --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/BaseSysUserService.java @@ -0,0 +1,25 @@ +package com.cool.modules.base.service.sys; + +import cn.hutool.core.lang.Dict; +import com.cool.core.base.BaseService; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; + +/** + * 系统用户 + */ +public interface BaseSysUserService extends BaseService { + /** + * 修改用户信息 + * + * @param body 用户信息 + */ + void personUpdate(Long userId, Dict body); + + /** + * 移动部门 + * + * @param departmentId 部门ID + * @param userIds 用户ID集合 + */ + void move(Long departmentId, Long[] userIds); +} diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysConfServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysConfServiceImpl.java new file mode 100644 index 0000000..db130a9 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysConfServiceImpl.java @@ -0,0 +1,58 @@ +package com.cool.modules.base.service.sys.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.cache.CoolCache; +import com.cool.modules.base.entity.sys.BaseSysConfEntity; +import com.cool.modules.base.mapper.sys.BaseSysConfMapper; +import com.cool.modules.base.service.sys.BaseSysConfService; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.update.UpdateChain; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 系统配置 + */ +@Service +@RequiredArgsConstructor +public class BaseSysConfServiceImpl extends BaseServiceImpl + implements BaseSysConfService { + + private final CoolCache coolCache; + + @Override + public void updateValue(String key, String value) { + UpdateChain.of(BaseSysConfEntity.class).set(BaseSysConfEntity::getCValue, value) + .eq(BaseSysConfEntity::getCKey, key).update(); + } + + @Override + public String getValue(String key) { + BaseSysConfEntity baseSysConfEntity = getOne(QueryWrapper.create().eq(BaseSysConfEntity::getCKey, key)); + if (baseSysConfEntity != null) { + return baseSysConfEntity.getCValue(); + } + return null; + } + + @Override + public String getValueWithCache(String key) { + String value = coolCache.get(key, String.class); + if (value != null) { + return value; + } + value = getValue(key); + if (value != null) { + coolCache.set(key, value); + } + return value; + } + + @Override + public void setValue(String key, String value) { + BaseSysConfEntity baseSysConfEntity = new BaseSysConfEntity(); + baseSysConfEntity.setCKey(key); + baseSysConfEntity.setCValue(value); + save(baseSysConfEntity); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysDepartmentServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysDepartmentServiceImpl.java new file mode 100644 index 0000000..c666b16 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysDepartmentServiceImpl.java @@ -0,0 +1,85 @@ +package com.cool.modules.base.service.sys.impl; + +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import com.cool.modules.base.mapper.sys.BaseSysDepartmentMapper; +import com.cool.modules.base.mapper.sys.BaseSysUserMapper; +import com.cool.modules.base.security.CoolSecurityUtil; +import com.cool.modules.base.service.sys.BaseSysDepartmentService; +import com.cool.modules.base.service.sys.BaseSysPermsService; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.update.UpdateChain; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 系统部门 + */ +@RequiredArgsConstructor +@Service +public class BaseSysDepartmentServiceImpl extends + BaseServiceImpl + implements BaseSysDepartmentService { + + final private BaseSysUserMapper baseSysUserMapper; + + final private CoolSecurityUtil coolSecurityUtil; + + final private BaseSysPermsService baseSysPermsService; + + @Override + public void order(List list) { + list.forEach(baseSysDepartmentEntity -> { + UpdateChain.of(BaseSysDepartmentEntity.class) + .set(BaseSysDepartmentEntity::getOrderNum, baseSysDepartmentEntity.getOrderNum()) + .eq(BaseSysDepartmentEntity::getId, baseSysDepartmentEntity.getId()).update(); + }); + } + + @Override + public List list(JSONObject requestParams, QueryWrapper queryWrapper) { + String username = coolSecurityUtil.username(); + Long[] loginDepartmentIds = baseSysPermsService.loginDepartmentIds(); + if (loginDepartmentIds != null && loginDepartmentIds.length == 0) { + return new ArrayList<>(); + } + List list = this.list( + QueryWrapper.create() + .in(BaseSysDepartmentEntity::getId, loginDepartmentIds, !username.equals("admin")) + .orderBy(BaseSysDepartmentEntity::getOrderNum, false)); + list.forEach(e -> { + List parentDepartment = list.stream() + .filter(sysDepartmentEntity -> e.getParentId() != null + && e.getParentId().equals(sysDepartmentEntity.getId())) + .toList(); + if (!parentDepartment.isEmpty()) { + e.setParentName(parentDepartment.get(0).getName()); + } + }); + return list; + } + + @Override + public boolean delete(JSONObject requestParams, Long... ids) { + super.delete(ids); + // 是否删除对应用户 否则移动到顶层部门 + if (requestParams.getBool("deleteUser")) { + return baseSysUserMapper + .deleteByQuery( + QueryWrapper.create().in(BaseSysUserEntity::getDepartmentId, (Object) ids)) > 0; + } else { + BaseSysDepartmentEntity topDepartment = getOne( + QueryWrapper.create().isNull(BaseSysDepartmentEntity::getParentId)); + if (topDepartment != null) { + UpdateChain.of(BaseSysUserEntity.class) + .set(BaseSysUserEntity::getDepartmentId, topDepartment.getId()) + .in(BaseSysUserEntity::getDepartmentId, (Object) ids).update(); + } + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLogServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLogServiceImpl.java new file mode 100644 index 0000000..9e4796c --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLogServiceImpl.java @@ -0,0 +1,95 @@ +package com.cool.modules.base.service.sys.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.util.IPUtils; +import com.cool.modules.base.entity.sys.BaseSysLogEntity; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import com.cool.modules.base.entity.sys.table.BaseSysLogEntityTableDef; +import com.cool.modules.base.entity.sys.table.BaseSysUserEntityTableDef; +import com.cool.modules.base.mapper.sys.BaseSysLogMapper; +import com.cool.modules.base.security.CoolSecurityUtil; +import com.cool.modules.base.service.sys.BaseSysConfService; +import com.cool.modules.base.service.sys.BaseSysLogService; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +/** + * 系统日志 + */ +@RequiredArgsConstructor +@Service +public class BaseSysLogServiceImpl extends BaseServiceImpl + implements BaseSysLogService { + + private final BaseSysConfService baseSysConfService; + + private final CoolSecurityUtil coolSecurityUtil; + + private final IPUtils ipUtils; + + @Override + public Object page( + JSONObject requestParams, Page page, QueryWrapper queryWrapper) { + queryWrapper + .select( + BaseSysLogEntityTableDef.BASE_SYS_LOG_ENTITY.ALL_COLUMNS, + BaseSysUserEntityTableDef.BASE_SYS_USER_ENTITY.NAME) + .from(BaseSysLogEntityTableDef.BASE_SYS_LOG_ENTITY) + .leftJoin(BaseSysUserEntityTableDef.BASE_SYS_USER_ENTITY) + .on(BaseSysLogEntity::getUserId, BaseSysUserEntity::getId); + return mapper.paginate(page, queryWrapper); + } + + @Override + public void clear(boolean isAll) { + if (isAll) { + this.remove(QueryWrapper.create().ge(BaseSysLogEntity::getId, 0)); + } else { + String keepDay = baseSysConfService.getValue("logKeep"); + int keepDays = Integer.parseInt(StrUtil.isNotEmpty(keepDay) ? keepDay : "30"); + Date beforeDate = DateUtil.offsetDay(new Date(), -keepDays); + this.remove(QueryWrapper.create().lt(BaseSysLogEntity::getCreateTime, beforeDate)); + } + } + + @Override + public void record(HttpServletRequest request, JSONObject requestParams) { + String requestURI = request.getRequestURI(); + String ipAddr = ipUtils.getIpAddr(request); + JSONObject userInfo = coolSecurityUtil.userInfo(requestParams); + + Long userId = null; + if (userInfo != null) { + userId = userInfo.getLong("userId"); + } + + JSONObject newJSONObject = JSONUtil.parseObj(JSONUtil.toJsonStr(requestParams)); + newJSONObject.remove("tokenInfo"); + newJSONObject.remove("refreshToken"); + newJSONObject.remove("body"); + + recordAsync(requestURI, ipAddr, userId, newJSONObject); + } + + + @Async + public void recordAsync(String requestURI, String ip, Long userId, JSONObject params) { + BaseSysLogEntity logEntity = new BaseSysLogEntity(); + logEntity.setAction(requestURI); + logEntity.setIp(ip); + if (userId != null) { + logEntity.setUserId(userId); + } + logEntity.setParams(params); + save(logEntity); + } +} diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLoginServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLoginServiceImpl.java new file mode 100644 index 0000000..41df62a --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLoginServiceImpl.java @@ -0,0 +1,137 @@ +package com.cool.modules.base.service.sys.impl; + +import cn.hutool.captcha.CaptchaUtil; +import cn.hutool.captcha.GifCaptcha; +import cn.hutool.captcha.generator.RandomGenerator; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWT; +import com.cool.core.cache.CoolCache; +import com.cool.core.exception.CoolException; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.security.jwt.JwtTokenUtil; +import com.cool.modules.base.dto.sys.BaseSysLoginDto; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import com.cool.modules.base.mapper.sys.BaseSysUserMapper; +import com.cool.modules.base.security.CoolSecurityUtil; +import com.cool.modules.base.service.sys.BaseSysLoginService; +import com.cool.modules.base.service.sys.BaseSysPermsService; +import com.mybatisflex.core.query.QueryWrapper; +import java.awt.Color; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class BaseSysLoginServiceImpl implements BaseSysLoginService { + + private final CoolCache coolCache; + + private final AuthenticationManager authenticationManager; + + private final CoolSecurityUtil coolSecurityUtil; + + private final JwtTokenUtil jwtTokenUtil; + + private final BaseSysUserMapper baseSysUserMapper; + + private final BaseSysPermsService baseSysPermsService; + + @Override + public Object captcha(String type, Integer width, Integer height) { + // 1、生成验证码 2、生成对应的ID并设置在缓存中,验证码过期时间30分钟; + Map result = new HashMap<>(); + String captchaId = StrUtil.uuid(); + result.put("captchaId", captchaId); + RandomGenerator randomGenerator = new RandomGenerator(4); + GifCaptcha gifCaptcha = CaptchaUtil.createGifCaptcha(width, height); + gifCaptcha.setGenerator(randomGenerator); + gifCaptcha.setBackground(new Color(248, 248, 248)); + gifCaptcha.setMaxColor(60); + gifCaptcha.setMinColor(55); + result.put("data", "data:image/png;base64," + gifCaptcha.getImageBase64()); + coolCache.set("verify:img:" + captchaId, gifCaptcha.getCode(), 1800); + return result; + } + + @Override + public Object login(BaseSysLoginDto baseSysLoginDto) { + // 1、检查验证码是否正确 2、执行登录操作 + String verifyCode = coolCache.get("verify:img:" + baseSysLoginDto.getCaptchaId(), + String.class); + if (StrUtil.isNotEmpty(verifyCode) + && verifyCode.equalsIgnoreCase(baseSysLoginDto.getVerifyCode())) { + UsernamePasswordAuthenticationToken upToken = + new UsernamePasswordAuthenticationToken( + baseSysLoginDto.getUsername(), baseSysLoginDto.getPassword()); + Authentication authentication = authenticationManager.authenticate(upToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + // 查询用户信息并生成token + BaseSysUserEntity sysUserEntity = + baseSysUserMapper.selectOneByQuery( + QueryWrapper.create() + .eq(BaseSysUserEntity::getUsername, baseSysLoginDto.getUsername())); + CoolPreconditions.check( + ObjectUtil.isEmpty(sysUserEntity) || sysUserEntity.getStatus() == 0, "用户已禁用"); + Long[] roleIds = baseSysPermsService.getRoles(sysUserEntity); + Dict tokenInfo = + Dict.create() + .set("roleIds", roleIds) + .set("username", baseSysLoginDto.getUsername()) + .set("userId", sysUserEntity.getId()) + .set("passwordVersion", sysUserEntity.getPasswordV()); + String token = jwtTokenUtil.generateToken(tokenInfo); + String refreshToken = jwtTokenUtil.generateRefreshToken(tokenInfo); + coolCache.del("verify:img:" + baseSysLoginDto.getCaptchaId()); + return Dict.create() + .set("token", token) + .set("expire", jwtTokenUtil.getExpire()) + .set("refreshToken", refreshToken) + .set("refreshExpire", jwtTokenUtil.getRefreshExpire()); + } else { + coolCache.del("verify:img:" + baseSysLoginDto.getCaptchaId()); + throw new CoolException("验证码不正确"); + } + } + + @Override + public void logout(Long adminUserId, String username) { + coolSecurityUtil.logout(adminUserId, username); + } + + @Override + public Object refreshToken(String refreshToken) { + JWT jwt = jwtTokenUtil.getTokenInfo(refreshToken); + try { + CoolPreconditions.check(jwt == null || !(Boolean) jwt.getPayload("isRefresh"), + "错误的token"); + + BaseSysUserEntity baseSysUserEntity = + baseSysUserMapper.selectOneById(Convert.toLong(jwt.getPayload("userId"))); + Long[] roleIds = baseSysPermsService.getRoles(baseSysUserEntity); + Dict tokenInfo = + Dict.create() + .set("roleIds", roleIds) + .set("username", baseSysUserEntity.getUsername()) + .set("userId", baseSysUserEntity.getId()) + .set("passwordVersion", baseSysUserEntity.getPasswordV()); + String token = jwtTokenUtil.generateToken(tokenInfo); + refreshToken = jwtTokenUtil.generateRefreshToken(tokenInfo); + return Dict.create() + .set("token", token) + .set("expire", jwtTokenUtil.getExpire()) + .set("refreshToken", refreshToken) + .set("refreshExpire", jwtTokenUtil.getRefreshExpire()); + } catch (Exception e) { + throw new CoolException("错误的token", e); + } + } +} diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysMenuServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysMenuServiceImpl.java new file mode 100644 index 0000000..fb07917 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysMenuServiceImpl.java @@ -0,0 +1,170 @@ +package com.cool.modules.base.service.sys.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.base.ModifyEnum; +import com.cool.core.util.CompilerUtils; +import com.cool.core.util.PathUtils; +import com.cool.modules.base.entity.sys.BaseSysMenuEntity; +import com.cool.modules.base.mapper.sys.BaseSysMenuMapper; +import com.cool.modules.base.security.CoolSecurityUtil; +import com.cool.modules.base.service.sys.BaseSysMenuService; +import com.cool.modules.base.service.sys.BaseSysPermsService; +import com.mybatisflex.core.query.QueryWrapper; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 系统菜单 + */ +@Service +@RequiredArgsConstructor +public class BaseSysMenuServiceImpl extends BaseServiceImpl + implements BaseSysMenuService { + + final private BaseSysPermsService baseSysPermsService; + + final private CoolSecurityUtil coolSecurityUtil; + + @Override + public Object list(JSONObject requestParams, QueryWrapper queryWrapper) { + List list = baseSysPermsService.getMenus(coolSecurityUtil.username()); + list.forEach(e -> { + List parent = list.stream() + .filter(sysMenuEntity -> e.getParentId() != null && e.getParentId() + .equals(sysMenuEntity.getId())) + .toList(); + if (!parent.isEmpty()) { + e.setParentName(parent.get(0).getName()); + } + }); + return list; + } + + @Override + public void modifyAfter(JSONObject requestParams, BaseSysMenuEntity sysMenuEntity, + ModifyEnum type) { + if (sysMenuEntity != null && sysMenuEntity.getId() != null) { + baseSysPermsService.refreshPermsByMenuId(requestParams.getLong("id")); + } + if (requestParams.get("ids") != null) { + Long[] ids = requestParams.get("ids", Long[].class); + for (Long id : ids) { + baseSysPermsService.refreshPermsByMenuId(id); + } + } + } + + @Override + public boolean delete(Long... ids) { + super.delete(ids); + for (Long id : ids) { + this.delChildMenu(id); + } + return true; + } + + /** + * 删除子菜单 + * + * @param id 删除的菜单ID + */ + private void delChildMenu(Long id) { + List delMenu = list( + QueryWrapper.create().eq(BaseSysMenuEntity::getParentId, id)); + if (CollectionUtil.isEmpty(delMenu)) { + return; + } + Long[] ids = delMenu.stream().map(BaseSysMenuEntity::getId).toArray(Long[]::new); + if (ArrayUtil.isNotEmpty(ids)) { + delete(ids); + for (Long delId : ids) { + this.delChildMenu(delId); + } + } + } + + @Override + public Object export(List ids) { + List list = list( + QueryWrapper.create().in(BaseSysMenuEntity::getId, ids)); + List parentList = list.stream() + .filter(o -> ObjUtil.isEmpty(o.getParentId())).toList(); + Map> map = list.stream() + .filter(o -> ObjUtil.isNotEmpty(o.getParentId())) + .collect(Collectors.groupingBy(BaseSysMenuEntity::getParentId)); + parentList.forEach(o -> handler(o, map)); + return parentList; + } + + private void handler(BaseSysMenuEntity parentBaseSysMenuEntity, + Map> map) { + parentBaseSysMenuEntity.setChildMenus( + map.getOrDefault(parentBaseSysMenuEntity.getId(), new ArrayList<>())); + parentBaseSysMenuEntity.getChildMenus().forEach(o -> { + handler(o, map); + o.setId(null); + o.setParentId(null); + o.setCreateTime(null); + o.setUpdateTime(null); + }); + parentBaseSysMenuEntity.setId(null); + parentBaseSysMenuEntity.setParentId(null); + parentBaseSysMenuEntity.setCreateTime(null); + parentBaseSysMenuEntity.setUpdateTime(null); + } + + @Override + public boolean importMenu(List menus) { + menus.forEach(this::importMenu); + return true; + } + + private void importMenu(BaseSysMenuEntity sysMenuEntity) { + sysMenuEntity.save(); + if (ObjUtil.isNotEmpty(sysMenuEntity.getChildMenus())) { + sysMenuEntity.getChildMenus().forEach(o -> { + o.setParentId(sysMenuEntity.getId()); + importMenu(o); + }); + } + } + + @Override + public void create(Map params) { + String module = (String) params.get("module"); + String controller = (String) params.get("controller"); + String entity = (String) params.get("entity"); + String service = (String) params.get("service"); + String serviceImpl = (String) params.get("service-impl"); + String mapper = (String) params.get("mapper"); + + String fileName = (String) params.get("fileName"); + String modulesPath = PathUtils.getModulesPath(); + // 创建的模块地址 + String actModulePath = CompilerUtils.createModule(modulesPath, module); + // 创建 controller + CompilerUtils.createController(actModulePath, fileName, controller); + // 创建 entity + String entityPath = CompilerUtils.createEntity(actModulePath, fileName, entity); + // 创建 service + CompilerUtils.createService(actModulePath, fileName, service); + // 创建 serviceImpl + CompilerUtils.createServiceImpl(actModulePath, fileName, serviceImpl); + // 创建 mapper + CompilerUtils.createMapper(actModulePath, fileName, mapper); + // 关闭springboot + System.exit(0); + // 构建TableDef + // CompilerUtils.compilerEntityTableDef(entityPath); + // 重启 + // CoolApplication.restart(); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysParamServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysParamServiceImpl.java new file mode 100644 index 0000000..30607c6 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysParamServiceImpl.java @@ -0,0 +1,53 @@ +package com.cool.modules.base.service.sys.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.cache.CoolCache; +import com.cool.modules.base.entity.sys.BaseSysParamEntity; +import com.cool.modules.base.mapper.sys.BaseSysParamMapper; +import com.cool.modules.base.service.sys.BaseSysParamService; +import com.mybatisflex.core.query.QueryWrapper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 系统参数配置 + */ +@Service +@RequiredArgsConstructor +public class BaseSysParamServiceImpl extends BaseServiceImpl + implements BaseSysParamService { + + final private CoolCache coolCache; + + @Override + public String htmlByKey(String key) { + String data = dataByKey(key); + return "" + (StrUtil.isNotEmpty(data) ? data : "key notfound") + + ""; + } + + @Override + public String dataByKey(String key) { + BaseSysParamEntity baseSysParamEntity = coolCache.get(key, BaseSysParamEntity.class); + if (baseSysParamEntity == null) { + baseSysParamEntity = getOne( + QueryWrapper.create().eq(BaseSysParamEntity::getKeyName, key)); + } + if (baseSysParamEntity != null) { + coolCache.set("param:" + baseSysParamEntity.getKeyName(), baseSysParamEntity); + return baseSysParamEntity.getData(); + } + return null; + } + + @Override + public void modifyAfter(JSONObject requestParams, BaseSysParamEntity baseSysParamEntity) { + List list = this.list(); + list.forEach(e -> { + coolCache.set("param:" + e.getKeyName(), e); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysPermsServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysPermsServiceImpl.java new file mode 100644 index 0000000..208550f --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysPermsServiceImpl.java @@ -0,0 +1,246 @@ +package com.cool.modules.base.service.sys.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.cool.core.cache.CoolCache; +import com.cool.core.util.SpringContextUtils; +import com.cool.modules.base.entity.sys.*; +import com.cool.modules.base.mapper.sys.*; +import com.cool.modules.base.security.CoolSecurityUtil; +import com.cool.modules.base.service.sys.BaseSysPermsService; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.row.Row; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +import java.util.*; + +import static com.cool.modules.base.entity.sys.table.BaseSysMenuEntityTableDef.BASE_SYS_MENU_ENTITY; +import static com.cool.modules.base.entity.sys.table.BaseSysRoleMenuEntityTableDef.BASE_SYS_ROLE_MENU_ENTITY; +import static com.cool.modules.base.entity.sys.table.BaseSysUserRoleEntityTableDef.BASE_SYS_USER_ROLE_ENTITY; + +@Service +@RequiredArgsConstructor +public class BaseSysPermsServiceImpl implements BaseSysPermsService { + final private CoolCache coolCache; + + final private BaseSysUserMapper baseSysUserMapper; + + final private BaseSysUserRoleMapper baseSysUserRoleMapper; + + final private BaseSysMenuMapper baseSysMenuMapper; + + final private BaseSysRoleMenuMapper baseSysRoleMenuMapper; + + final private BaseSysRoleDepartmentMapper baseSysRoleDepartmentMapper; + + final private BaseSysDepartmentMapper baseSysDepartmentMapper; + + final private CoolSecurityUtil coolSecurityUtil; + + @Override + public Long[] loginDepartmentIds() { + String username = coolSecurityUtil.username(); + if (username.equals("admin")) { + return baseSysDepartmentMapper.selectAll().stream().map(BaseSysDepartmentEntity::getId) + .toArray(Long[]::new); + } else { + Long[] roleIds = getRoles(username); + return baseSysRoleDepartmentMapper + .selectListByQuery( + QueryWrapper.create().in(BaseSysRoleDepartmentEntity::getRoleId, (Object) roleIds)) + .stream().map(BaseSysRoleDepartmentEntity::getDepartmentId).toArray(Long[]::new); + } + } + + @Override + public Long[] getDepartmentIdsByRoleIds(Long[] roleIds) { + return getLongs(roleIds); + } + + private Long[] getLongs(Long[] roleIds) { + return baseSysRoleDepartmentMapper + .selectListByQuery(QueryWrapper.create().in(BaseSysRoleDepartmentEntity::getRoleId, (Object) roleIds, + roleIds != null && !CollUtil.toList(roleIds).contains(1L))) + .stream().map(BaseSysRoleDepartmentEntity::getDepartmentId).toArray(Long[]::new); + } + + @Override + public Long[] getDepartmentIdsByRoleIds(Long userId) { + Long[] roleIds = getRoles(userId); + return getLongs(roleIds); + } + + @Override + public String[] getPermsCache(Long userId) { + Object result = coolCache.get("admin:perms:" + userId); + if (ObjectUtil.isNotEmpty(result)) { + return Convert.toStrArray(result); + } + return getPerms(userId); + } + + @Override + public Long[] getRoles(Long userId) { + return getRoles(baseSysUserMapper.selectOneById(userId)); + } + + @Override + public Long[] getRoles(String username) { + return getRoles( + baseSysUserMapper.selectOneByQuery(QueryWrapper.create().eq(BaseSysUserEntity::getUsername, username))); + } + + @Override + public Long[] getRoles(BaseSysUserEntity userEntity) { + Long[] roleIds = null; + if (!userEntity.getUsername().equals("admin")) { + List list = baseSysUserRoleMapper + .selectListByQuery(QueryWrapper.create().eq(BaseSysUserRoleEntity::getUserId, userEntity.getId())); + roleIds = list.stream().map(BaseSysUserRoleEntity::getRoleId).toArray(Long[]::new); + if (Arrays.asList(roleIds).contains(1L)) { + roleIds = null; + } + } + return roleIds; + } + + @Override + public String[] getPerms(Long userId) { + return getPerms(getRoles(userId)); + } + + @Override + public String[] getPerms(Long[] roleIds) { + List menus = getMenus(roleIds); + Set perms = new HashSet<>(); + String[] permsData = menus.stream().map(BaseSysMenuEntity::getPerms) + .filter(itemPerms -> !StrUtil.isEmpty(itemPerms)).toArray(String[]::new); + for (String permData : permsData) { + perms.addAll(Arrays.asList(permData.split(","))); + } + return ArrayUtil.toArray(perms, String.class); + } + + @Override + public List getMenus(Long[] roleIds) { + if (CollUtil.toList(roleIds).contains(1L)) { + roleIds = null; + } + if (roleIds != null && roleIds.length == 0) { + return new ArrayList<>(); + } + + QueryWrapper queryWrapper = QueryWrapper.create().select(BASE_SYS_MENU_ENTITY.ALL_COLUMNS).from(BASE_SYS_MENU_ENTITY); + if (ObjectUtil.isNotEmpty(roleIds)) { + queryWrapper.join(BASE_SYS_ROLE_MENU_ENTITY).on(BASE_SYS_MENU_ENTITY.ID.eq(BASE_SYS_ROLE_MENU_ENTITY.MENU_ID)).and(BASE_SYS_ROLE_MENU_ENTITY.ROLE_ID.in((Object) roleIds)); + } + return baseSysMenuMapper.selectListByQuery(queryWrapper.groupBy(BASE_SYS_MENU_ENTITY.ID).orderBy(BASE_SYS_MENU_ENTITY.ORDER_NUM, false)); + } + + @Override + public List getMenus(Long userId) { + return getMenus(getRoles(userId)); + } + + @Override + public List getMenus(String username) { + BaseSysUserEntity sysUserEntity = baseSysUserMapper + .selectOneByQuery(QueryWrapper.create().eq(BaseSysUserEntity::getUsername, username)); + return getMenus(sysUserEntity.getId()); + } + + @Override + public String[] getAllPerms() { + return getPerms((Long[]) null); + } + + @Override + public Object permmenu(Long adminUserId) { + return Dict.create().set("menus", getMenus(adminUserId)).set("perms", getPerms(adminUserId)); + } + + @Override + public void updatePerms(Long roleId, Long[] menuIdList, Long[] departmentIds) { + // 更新菜单权限 + baseSysRoleMenuMapper.deleteByQuery(QueryWrapper.create().eq(BaseSysRoleMenuEntity::getRoleId, roleId)); + for (Long menuId : menuIdList) { + BaseSysRoleMenuEntity roleMenuEntity = new BaseSysRoleMenuEntity(); + roleMenuEntity.setRoleId(roleId); + roleMenuEntity.setMenuId(menuId); + baseSysRoleMenuMapper.insert(roleMenuEntity); + } + // 更新部门权限 + baseSysRoleDepartmentMapper + .deleteByQuery(QueryWrapper.create().eq(BaseSysRoleDepartmentEntity::getRoleId, roleId)); + for (Long departmentId : departmentIds) { + BaseSysRoleDepartmentEntity roleDepartmentEntity = new BaseSysRoleDepartmentEntity(); + roleDepartmentEntity.setRoleId(roleId); + roleDepartmentEntity.setDepartmentId(departmentId); + baseSysRoleDepartmentMapper.insert(roleDepartmentEntity); + } + // 刷新对应角色用户的权限 + List userRoles = baseSysUserRoleMapper + .selectListByQuery(QueryWrapper.create().eq(BaseSysUserRoleEntity::getRoleId, roleId)); + for (BaseSysUserRoleEntity userRole : userRoles) { + refreshPerms(userRole.getUserId()); + } + } + + @Override + public void updateUserRole(Long userId, Long[] roleIdList) { + baseSysUserRoleMapper.deleteByQuery(QueryWrapper.create().eq(BaseSysUserRoleEntity::getUserId, userId)); + if (roleIdList == null) { + roleIdList = new Long[0]; + } + for (Long roleId : roleIdList) { + BaseSysUserRoleEntity sysUserRoleEntity = new BaseSysUserRoleEntity(); + sysUserRoleEntity.setRoleId(roleId); + sysUserRoleEntity.setUserId(userId); + baseSysUserRoleMapper.insert(sysUserRoleEntity); + } + refreshPerms(userId); + } + + @Override + public void refreshPerms(Long userId) { + BaseSysUserEntity baseSysUserEntity = baseSysUserMapper.selectOneById(userId); + if (baseSysUserEntity != null && baseSysUserEntity.getStatus() != 0) { + SpringContextUtils.getBean(UserDetailsService.class).loadUserByUsername(baseSysUserEntity.getUsername()); + } + if (baseSysUserEntity != null && baseSysUserEntity.getStatus() == 0) { + coolSecurityUtil.logout(baseSysUserEntity.getId(), baseSysUserEntity.getUsername()); + } + } + + @Async + @Override + public void refreshPermsByMenuId(Long menuId) { + // 刷新超管权限、 找出这个菜单的所有用户、 刷新用户权限 + BaseSysUserEntity admin = baseSysUserMapper + .selectOneByQuery(QueryWrapper.create().eq(BaseSysUserEntity::getUsername, "admin")); + refreshPerms(admin.getId()); + List list = baseSysRoleMenuMapper.selectRowsByQuery(QueryWrapper.create().select(BASE_SYS_USER_ROLE_ENTITY.USER_ID) + .from(BASE_SYS_ROLE_MENU_ENTITY).leftJoin(BASE_SYS_USER_ROLE_ENTITY) + .on(BASE_SYS_ROLE_MENU_ENTITY.ROLE_ID.eq(BASE_SYS_USER_ROLE_ENTITY.ROLE_ID)).and(BASE_SYS_ROLE_MENU_ENTITY.MENU_ID.eq(menuId, ObjectUtil.isNotEmpty(menuId))).groupBy(BASE_SYS_USER_ROLE_ENTITY.USER_ID)); + for (Row row : list) { + refreshPerms(row.getLong("userId")); + } + } + + @Override + public void refreshPermsByRoleId(Long roleId) { + // 找出角色对应的所有用户 + List list = baseSysUserRoleMapper + .selectListByQuery(QueryWrapper.create().eq(BaseSysUserRoleEntity::getRoleId, roleId)); + list.forEach(e -> { + refreshPerms(e.getUserId()); + }); + } +} diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysRoleServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysRoleServiceImpl.java new file mode 100644 index 0000000..8c5a762 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysRoleServiceImpl.java @@ -0,0 +1,92 @@ +package com.cool.modules.base.service.sys.impl; + +import cn.hutool.core.lang.Dict; +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.base.ModifyEnum; +import com.cool.core.exception.CoolException; +import com.cool.modules.base.entity.sys.BaseSysRoleDepartmentEntity; +import com.cool.modules.base.entity.sys.BaseSysRoleEntity; +import com.cool.modules.base.entity.sys.BaseSysRoleMenuEntity; +import com.cool.modules.base.mapper.sys.BaseSysRoleDepartmentMapper; +import com.cool.modules.base.mapper.sys.BaseSysRoleMapper; +import com.cool.modules.base.mapper.sys.BaseSysRoleMenuMapper; +import com.cool.modules.base.security.CoolSecurityUtil; +import com.cool.modules.base.service.sys.BaseSysPermsService; +import com.cool.modules.base.service.sys.BaseSysRoleService; +import com.mybatisflex.core.query.QueryWrapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 系统角色 + */ +@RequiredArgsConstructor +@Service +public class BaseSysRoleServiceImpl extends BaseServiceImpl + implements BaseSysRoleService { + + final private BaseSysRoleMapper baseSysRoleMapper; + + final private BaseSysRoleMenuMapper baseSysRoleMenuMapper; + + final private BaseSysRoleDepartmentMapper baseSysRoleDepartmentMapper; + + final private BaseSysPermsService baseSysPermsService; + + final private CoolSecurityUtil coolSecurityUtil; + + @Override + public Object add(JSONObject requestParams, BaseSysRoleEntity entity) { + BaseSysRoleEntity checkLabel = getOne(QueryWrapper.create().eq(BaseSysRoleEntity::getLabel, entity.getLabel())); + if (checkLabel != null) { + throw new CoolException("标识已存在"); + } + entity.setUserId((coolSecurityUtil.userInfo(requestParams).getLong("userId"))); + return super.add(requestParams, entity); + } + + @Override + public Object info(Long id) { + BaseSysRoleEntity roleEntity = getById(id); + Long[] menuIdList = new Long[0]; + Long[] departmentIdList = new Long[0]; + if (roleEntity != null) { + List list = baseSysRoleMenuMapper + .selectListByQuery(QueryWrapper.create().eq(BaseSysRoleMenuEntity::getRoleId, id, !id.equals(1L))); + menuIdList = list.stream().map(BaseSysRoleMenuEntity::getMenuId).toArray(Long[]::new); + + List departmentEntities = baseSysRoleDepartmentMapper.selectListByQuery( + QueryWrapper.create().eq(BaseSysRoleDepartmentEntity::getRoleId, id, !id.equals(1L))); + + departmentIdList = departmentEntities.stream().map(BaseSysRoleDepartmentEntity::getDepartmentId) + .toArray(Long[]::new); + } + return Dict.parse(roleEntity).set("menuIdList", menuIdList).set("departmentIdList", departmentIdList); + } + + @Override + public void modifyAfter(JSONObject requestParams, BaseSysRoleEntity baseSysRoleEntity, ModifyEnum type) { + if (type == ModifyEnum.DELETE) { + Long[] ids = requestParams.get("ids", Long[].class); + for (Long id : ids) { + baseSysPermsService.refreshPermsByRoleId(id); + } + } else { + baseSysPermsService.updatePerms(baseSysRoleEntity.getId(), requestParams.get("menuIdList", Long[].class), + requestParams.get("departmentIdList", Long[].class)); + } + } + + @Override + public Object list(JSONObject requestParams, QueryWrapper queryWrapper) { + return baseSysRoleMapper.selectListByQuery(queryWrapper.ne(BaseSysRoleEntity::getId, 1L).and(qw -> { + qw.eq(BaseSysRoleEntity::getUserId, coolSecurityUtil.userInfo(requestParams).get("userId")).or(w -> { + w.in(BaseSysRoleEntity::getId, + (Object) coolSecurityUtil.userInfo(requestParams).get("roleIds", Long[].class)); + }); + }, !coolSecurityUtil.username().equals("admin"))); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysUserServiceImpl.java b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysUserServiceImpl.java new file mode 100644 index 0000000..7cac719 --- /dev/null +++ b/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysUserServiceImpl.java @@ -0,0 +1,161 @@ +package com.cool.modules.base.service.sys.impl; + +import static com.cool.modules.base.entity.sys.table.BaseSysDepartmentEntityTableDef.BASE_SYS_DEPARTMENT_ENTITY; +import static com.cool.modules.base.entity.sys.table.BaseSysRoleEntityTableDef.BASE_SYS_ROLE_ENTITY; +import static com.cool.modules.base.entity.sys.table.BaseSysUserEntityTableDef.BASE_SYS_USER_ENTITY; +import static com.cool.modules.base.entity.sys.table.BaseSysUserRoleEntityTableDef.BASE_SYS_USER_ROLE_ENTITY; +import static com.mybatisflex.core.query.QueryMethods.groupConcat; + +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.MD5; +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.base.ModifyEnum; +import com.cool.core.cache.CoolCache; +import com.cool.core.exception.CoolPreconditions; +import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import com.cool.modules.base.mapper.sys.BaseSysDepartmentMapper; +import com.cool.modules.base.mapper.sys.BaseSysUserMapper; +import com.cool.modules.base.security.CoolSecurityUtil; +import com.cool.modules.base.service.sys.BaseSysPermsService; +import com.cool.modules.base.service.sys.BaseSysUserService; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.update.UpdateChain; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 系统用户 + */ +@Service +@RequiredArgsConstructor +public class BaseSysUserServiceImpl extends BaseServiceImpl + implements BaseSysUserService { + + final private CoolCache coolCache; + + final private CoolSecurityUtil coolSecurityUtil; + + final private BaseSysPermsService baseSysPermsService; + + final private BaseSysDepartmentMapper baseSysDepartmentMapper; + + @Override + public Object page(JSONObject requestParams, Page page, QueryWrapper qw) { + String keyWord = requestParams.getStr("keyWord"); + Integer status = requestParams.getInt("status"); + Long[] departmentIds = requestParams.get("departmentIds", Long[].class); + JSONObject tokenInfo = coolSecurityUtil.userInfo(requestParams); + // 用户的部门权限 + Long[] permsDepartmentArr = coolCache.get("admin:department:" + tokenInfo.get("userId"), + Long[].class); + + qw.select(BASE_SYS_USER_ENTITY.ALL_COLUMNS, + groupConcat(BASE_SYS_ROLE_ENTITY.NAME).as("roleName"), + BASE_SYS_DEPARTMENT_ENTITY.NAME.as("departmentName")) + .from(BASE_SYS_USER_ENTITY).leftJoin(BASE_SYS_USER_ROLE_ENTITY) + .on(BASE_SYS_USER_ENTITY.ID.eq(BASE_SYS_USER_ROLE_ENTITY.USER_ID)) + .leftJoin(BASE_SYS_ROLE_ENTITY) + .on(BASE_SYS_USER_ROLE_ENTITY.USER_ID.eq(BASE_SYS_ROLE_ENTITY.ID)) + .leftJoin(BASE_SYS_DEPARTMENT_ENTITY) + .on(BASE_SYS_USER_ENTITY.DEPARTMENT_ID.eq(BASE_SYS_DEPARTMENT_ENTITY.ID)); + + // 不显示admin用户 + qw.and(BASE_SYS_USER_ENTITY.USERNAME.ne("admin")); + // 筛选部门 + qw.and(BASE_SYS_USER_ENTITY.DEPARTMENT_ID.in(departmentIds, + ArrayUtil.isNotEmpty(departmentIds))); + // 筛选状态 + qw.and(BASE_SYS_USER_ENTITY.STATUS.eq(status, status != null)); + // 搜索关键字 + if (StrUtil.isNotEmpty(keyWord)) { + qw.and(BASE_SYS_USER_ENTITY.NAME.like(keyWord) + .or(BASE_SYS_USER_ENTITY.USERNAME.like(keyWord))); + } + // 过滤部门权限 + qw.and(BASE_SYS_USER_ENTITY.DEPARTMENT_ID.in( + permsDepartmentArr == null || permsDepartmentArr.length == 0 ? new Long[]{null} + : permsDepartmentArr, + !coolSecurityUtil.username().equals("admin"))); + + qw.groupBy(BASE_SYS_USER_ENTITY.ID); + return mapper.paginateWithRelations(page, qw); + } + + @Override + public void personUpdate(Long userId, Dict body) { + BaseSysUserEntity userEntity = getById(userId); + CoolPreconditions.checkEmpty(userEntity, "用户不存在"); + userEntity.setNickName(body.getStr("nickName")); + // 修改密码 + if (StrUtil.isNotEmpty(body.getStr("password"))) { + userEntity.setPassword(MD5.create().digestHex(body.getStr("password"))); + userEntity.setPasswordV(userEntity.getPasswordV() + 1); + coolCache.set("admin:passwordVersion:" + userId, userEntity.getPasswordV()); + } + updateById(userEntity); + } + + @Override + public void move(Long departmentId, Long[] userIds) { + UpdateChain.of(BaseSysUserEntity.class) + .set(BaseSysUserEntity::getDepartmentId, departmentId) + .in(BaseSysUserEntity::getId, (Object) userIds).update(); + } + + @Override + public Long add(JSONObject requestParams, BaseSysUserEntity entity) { + BaseSysUserEntity check = getOne( + QueryWrapper.create().eq(BaseSysUserEntity::getUsername, entity.getUsername())); + CoolPreconditions.check(check != null, "用户名已存在"); + entity.setPassword(MD5.create().digestHex(entity.getPassword())); + super.add(requestParams, entity); + return entity.getId(); + } + + @Override + public boolean update(JSONObject requestParams, BaseSysUserEntity entity) { + CoolPreconditions.check( + StrUtil.isNotEmpty(entity.getUsername()) && entity.getUsername().equals("admin"), + "非法操作"); + BaseSysUserEntity userEntity = getById(entity.getId()); + if (StrUtil.isNotEmpty(entity.getPassword())) { + entity.setPasswordV(entity.getPasswordV() + 1); + entity.setPassword(MD5.create().digestHex(entity.getPassword())); + coolCache.set("admin:passwordVersion:" + entity.getId(), entity.getPasswordV()); + } else { + entity.setPassword(userEntity.getPassword()); + entity.setPasswordV(userEntity.getPasswordV()); + } + // 被禁用 + if (entity.getStatus() == 0) { + coolSecurityUtil.logout(entity); + } + return super.update(requestParams, entity); + } + + @Override + public void modifyAfter(JSONObject requestParams, BaseSysUserEntity baseSysUserEntity, + ModifyEnum type) { + if (type != ModifyEnum.DELETE && requestParams.get("roleIdList", Long[].class) != null) { + // 刷新权限 + baseSysPermsService.updateUserRole(baseSysUserEntity.getId(), + requestParams.get("roleIdList", Long[].class)); + } + } + + @Override + public Object info(Long id) { + BaseSysUserEntity userEntity = getById(id); + Long[] roleIdList = baseSysPermsService.getRoles(id); + BaseSysDepartmentEntity departmentEntity = baseSysDepartmentMapper.selectOneById( + userEntity.getDepartmentId()); + userEntity.setPassword(null); + return Dict.parse(userEntity).set("roleIdList", roleIdList).set("departmentName", + departmentEntity != null ? departmentEntity.getName() : null); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/dict/controller/admin/AdminDictInfoController.java b/src/main/java/com/cool/modules/dict/controller/admin/AdminDictInfoController.java new file mode 100644 index 0000000..6ad3483 --- /dev/null +++ b/src/main/java/com/cool/modules/dict/controller/admin/AdminDictInfoController.java @@ -0,0 +1,37 @@ +package com.cool.modules.dict.controller.admin; + +import cn.hutool.core.lang.Dict; +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.dict.entity.DictInfoEntity; +import com.cool.modules.dict.entity.table.DictInfoEntityTableDef; +import com.cool.modules.dict.service.DictInfoService; +import com.mybatisflex.core.query.QueryWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 字典信息 + */ +@Tag(name = "字典信息", description = "字典信息") +@CoolRestController(api = { "add", "delete", "update", "page", "list", "info" }) +public class AdminDictInfoController extends BaseController { + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setListOption(createOp().fieldEq(DictInfoEntityTableDef.DICT_INFO_ENTITY.TYPE_ID) + .keyWordLikeFields(DictInfoEntityTableDef.DICT_INFO_ENTITY.NAME) + .queryWrapper(QueryWrapper.create().orderBy(DictInfoEntityTableDef.DICT_INFO_ENTITY.CREATE_TIME, false))); + } + + @Operation(summary = "获得字典数据", description = "获得字典数据信息") + @PostMapping("/data") + public R data(@RequestBody Dict body) { + return R.ok(this.service.data(body.get("types", null))); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/dict/controller/admin/AdminDictTypeController.java b/src/main/java/com/cool/modules/dict/controller/admin/AdminDictTypeController.java new file mode 100644 index 0000000..0c9f791 --- /dev/null +++ b/src/main/java/com/cool/modules/dict/controller/admin/AdminDictTypeController.java @@ -0,0 +1,25 @@ +package com.cool.modules.dict.controller.admin; + +import static com.cool.modules.dict.entity.table.DictTypeEntityTableDef.DICT_TYPE_ENTITY; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.dict.entity.DictTypeEntity; +import com.cool.modules.dict.service.DictTypeService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; + +/** + * 字典类型 + */ +@Tag(name = "字典类型", description = "字典类型") +@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}) +public class AdminDictTypeController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption( + createOp().select(DICT_TYPE_ENTITY.ID, DICT_TYPE_ENTITY.KEY, DICT_TYPE_ENTITY.NAME)); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/dict/controller/app/AppDictInfoController.java b/src/main/java/com/cool/modules/dict/controller/app/AppDictInfoController.java new file mode 100644 index 0000000..bad4ba8 --- /dev/null +++ b/src/main/java/com/cool/modules/dict/controller/app/AppDictInfoController.java @@ -0,0 +1,34 @@ +package com.cool.modules.dict.controller.app; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Dict; +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.dict.entity.DictInfoEntity; +import com.cool.modules.dict.service.DictInfoService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 字典信息 + */ +@Tag(name = "字典信息", description = "字典信息") +@CoolRestController(api = {}) +public class AppDictInfoController extends BaseController { + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + + } + + @Operation(summary = "获得字典数据", description = "获得字典数据信息") + @PostMapping("/data") + public R data(@RequestBody Dict body) { + return R.ok(this.service.data(Convert.toList(String.class, body.get("types")))); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/dict/entity/DictInfoEntity.java b/src/main/java/com/cool/modules/dict/entity/DictInfoEntity.java new file mode 100644 index 0000000..ff0c944 --- /dev/null +++ b/src/main/java/com/cool/modules/dict/entity/DictInfoEntity.java @@ -0,0 +1,32 @@ +package com.cool.modules.dict.entity; + +import com.cool.core.base.BaseEntity; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "dict_info", comment = "字典信息") +public class DictInfoEntity extends BaseEntity { + + @ColumnDefine(comment = "类型ID", notNull = true) + private Long typeId; + + @ColumnDefine(comment = "父ID") + private Long parentId; + + @ColumnDefine(comment = "名称", notNull = true) + private String name; + + @ColumnDefine(comment = "值") + private String value; + + @ColumnDefine(comment = "排序", defaultValue = "0") + private Integer orderNum; + + @ColumnDefine(comment = "备注") + private String remark; + +} diff --git a/src/main/java/com/cool/modules/dict/entity/DictTypeEntity.java b/src/main/java/com/cool/modules/dict/entity/DictTypeEntity.java new file mode 100644 index 0000000..35a2fd2 --- /dev/null +++ b/src/main/java/com/cool/modules/dict/entity/DictTypeEntity.java @@ -0,0 +1,21 @@ +package com.cool.modules.dict.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.tangzc.mybatisflex.autotable.annotation.UniIndex; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "dict_type", comment = "字典类型") +public class DictTypeEntity extends BaseEntity { + + @ColumnDefine(comment = "名称", notNull = true) + private String name; + + @ColumnDefine(comment = "标识", notNull = true) + @UniIndex + private String key; +} diff --git a/src/main/java/com/cool/modules/dict/mapper/DictInfoMapper.java b/src/main/java/com/cool/modules/dict/mapper/DictInfoMapper.java new file mode 100644 index 0000000..6c79be5 --- /dev/null +++ b/src/main/java/com/cool/modules/dict/mapper/DictInfoMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.dict.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.dict.entity.DictInfoEntity; + +/** + * 字典信息 + */ +public interface DictInfoMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/dict/mapper/DictTypeMapper.java b/src/main/java/com/cool/modules/dict/mapper/DictTypeMapper.java new file mode 100644 index 0000000..1c01714 --- /dev/null +++ b/src/main/java/com/cool/modules/dict/mapper/DictTypeMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.dict.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.dict.entity.DictTypeEntity; + +/** + * 字典类型 + */ +public interface DictTypeMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/dict/service/DictInfoService.java b/src/main/java/com/cool/modules/dict/service/DictInfoService.java new file mode 100644 index 0000000..7b5db8a --- /dev/null +++ b/src/main/java/com/cool/modules/dict/service/DictInfoService.java @@ -0,0 +1,19 @@ +package com.cool.modules.dict.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.dict.entity.DictInfoEntity; + +import java.util.List; + +/** + * 字典信息 + */ +public interface DictInfoService extends BaseService { + /** + * 字典数据 + * + * @param types 字典类型 + * @return + */ + Object data(List types); +} diff --git a/src/main/java/com/cool/modules/dict/service/DictTypeService.java b/src/main/java/com/cool/modules/dict/service/DictTypeService.java new file mode 100644 index 0000000..0c17f82 --- /dev/null +++ b/src/main/java/com/cool/modules/dict/service/DictTypeService.java @@ -0,0 +1,10 @@ +package com.cool.modules.dict.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.dict.entity.DictTypeEntity; + +/** + * 字典类型 + */ +public interface DictTypeService extends BaseService { +} diff --git a/src/main/java/com/cool/modules/dict/service/impl/DictInfoServiceImpl.java b/src/main/java/com/cool/modules/dict/service/impl/DictInfoServiceImpl.java new file mode 100644 index 0000000..11caced --- /dev/null +++ b/src/main/java/com/cool/modules/dict/service/impl/DictInfoServiceImpl.java @@ -0,0 +1,100 @@ +package com.cool.modules.dict.service.impl; + +import static com.cool.modules.dict.entity.table.DictInfoEntityTableDef.DICT_INFO_ENTITY; +import static com.cool.modules.dict.entity.table.DictTypeEntityTableDef.DICT_TYPE_ENTITY; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.dict.entity.DictInfoEntity; +import com.cool.modules.dict.entity.DictTypeEntity; +import com.cool.modules.dict.mapper.DictInfoMapper; +import com.cool.modules.dict.mapper.DictTypeMapper; +import com.cool.modules.dict.service.DictInfoService; +import com.mybatisflex.core.query.QueryWrapper; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 字典信息 + */ +@Service +@RequiredArgsConstructor +public class DictInfoServiceImpl extends BaseServiceImpl implements + DictInfoService { + + final private DictTypeMapper dictTypeMapper; + + @Override + public Object data(List types) { + Dict result = Dict.create(); + QueryWrapper find = QueryWrapper.create(); + find.select(DICT_TYPE_ENTITY.ID, DICT_TYPE_ENTITY.KEY, + DICT_TYPE_ENTITY.NAME); + if (CollectionUtil.isNotEmpty(types)) { + find.and(DICT_TYPE_ENTITY.KEY.in(types)); + } + List typeData = dictTypeMapper.selectListByQuery(find); + if (typeData.isEmpty()) { + return result; + } + List infos = this.list(QueryWrapper.create() + .select(DictInfoEntity::getId, DictInfoEntity::getName, DictInfoEntity::getTypeId, + DictInfoEntity::getParentId, DictInfoEntity::getValue) + .in(DictInfoEntity::getTypeId, + typeData.stream().map(DictTypeEntity::getId).collect(Collectors.toList())) + .orderBy(DICT_INFO_ENTITY.ORDER_NUM.getName(), DICT_INFO_ENTITY.CREATE_TIME.getName())); + typeData.forEach(item -> { + List datas = new ArrayList<>(); + infos.stream().filter(d -> d.getTypeId().equals(item.getId())).toList().forEach(d -> { + Dict data = Dict.create(); + data.set("typeId", d.getTypeId()); + data.set("parentId", d.getParentId()); + data.set("name", d.getName()); + data.set("id", d.getId()); + data.set("value", StrUtil.isEmpty(d.getValue()) ? null : d.getValue()); + try { + data.set("value", Integer.parseInt(d.getValue())); + } catch (Exception ignored) { + } + datas.add(data); + }); + result.set(item.getKey(), datas); + }); + return result; + } + + @Override + public boolean delete(Long... ids) { + super.delete(ids); + for (Long id : ids) { + this.delDictChild(id); + } + return true; + } + + /** + * 删除子菜单 + * + * @param id 删除的菜单ID + */ + private void delDictChild(Long id) { + List delDict = list( + QueryWrapper.create().eq(DictInfoEntity::getParentId, id)); + if (CollectionUtil.isEmpty(delDict)) { + return; + } + Long[] ids = delDict.stream().map(DictInfoEntity::getId).toArray(Long[]::new); + if (ArrayUtil.isNotEmpty(ids)) { + delete(ids); + for (Long delId : ids) { + this.delDictChild(delId); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/dict/service/impl/DictTypeServiceImpl.java b/src/main/java/com/cool/modules/dict/service/impl/DictTypeServiceImpl.java new file mode 100644 index 0000000..3655b2b --- /dev/null +++ b/src/main/java/com/cool/modules/dict/service/impl/DictTypeServiceImpl.java @@ -0,0 +1,39 @@ +package com.cool.modules.dict.service.impl; + +import static com.cool.modules.dict.entity.table.DictInfoEntityTableDef.DICT_INFO_ENTITY; +import static com.cool.modules.dict.entity.table.DictTypeEntityTableDef.DICT_TYPE_ENTITY; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.dict.entity.DictTypeEntity; +import com.cool.modules.dict.mapper.DictInfoMapper; +import com.cool.modules.dict.mapper.DictTypeMapper; +import com.cool.modules.dict.service.DictTypeService; +import com.mybatisflex.core.query.QueryWrapper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 字典类型 + */ +@Service +@RequiredArgsConstructor +public class DictTypeServiceImpl extends BaseServiceImpl implements + DictTypeService { + + final private DictInfoMapper dictInfoMapper; + + @Override + public List list(QueryWrapper queryWrapper) { + return super.list( + queryWrapper.select(DICT_TYPE_ENTITY.ID, DICT_TYPE_ENTITY.KEY, + DICT_TYPE_ENTITY.NAME)); + } + + @Override + public boolean delete(Long... ids) { + super.delete(ids); + return dictInfoMapper.deleteByQuery( + QueryWrapper.create().and(DICT_INFO_ENTITY.TYPE_ID.in((Object) ids))) > 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/plugin/controller/admin/AdminPluginInfoController.java b/src/main/java/com/cool/modules/plugin/controller/admin/AdminPluginInfoController.java new file mode 100644 index 0000000..7ca7b42 --- /dev/null +++ b/src/main/java/com/cool/modules/plugin/controller/admin/AdminPluginInfoController.java @@ -0,0 +1,72 @@ +package com.cool.modules.plugin.controller.admin; + +import static com.cool.modules.plugin.entity.table.PluginInfoEntityTableDef.PLUGIN_INFO_ENTITY; + +import cn.hutool.core.convert.Convert; +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.annotation.IgnoreRecycleData; +import com.cool.core.base.BaseController; +import com.cool.core.plugin.service.CoolPluginService; +import com.cool.core.request.R; +import com.cool.core.util.EntityUtils; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import com.cool.modules.plugin.service.PluginInfoService; +import com.mybatisflex.core.query.QueryWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "插件信息", description = "插件信息") +@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}) +@RequiredArgsConstructor +public class AdminPluginInfoController extends BaseController { + + final private CoolPluginService coolPluginService; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + + setPageOption(createOp().queryWrapper( + QueryWrapper.create().orderBy(PLUGIN_INFO_ENTITY.UPDATE_TIME, false)) + .select(EntityUtils.getFieldNamesWithSuperClass(PLUGIN_INFO_ENTITY.DEFAULT_COLUMNS, + PLUGIN_INFO_ENTITY.JAR_FILE.getName()))); + } + + @Override + @Operation(summary = "修改", description = "根据ID修改") + @PostMapping("/update") + protected R update(@RequestBody PluginInfoEntity t, + @RequestAttribute() JSONObject requestParams) { + coolPluginService.updatePlugin(t); + return R.ok(); + } + + @Operation(summary = "安装插件") + @PostMapping(value = "/install", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R install( + @RequestParam(value = "files") @Parameter(description = "文件") MultipartFile[] files, + @RequestParam(value = "force") @Parameter(description = "是否强制安装") boolean force) { + coolPluginService.install(files[0], force); + return R.ok(); + } + + @Override + @Operation(summary = "卸载插件") + @PostMapping("/delete") + @IgnoreRecycleData() + public R delete(HttpServletRequest request, @RequestBody Map params, + @RequestAttribute() JSONObject requestParams) { + coolPluginService.uninstall(Convert.toLongArray(getIds(params))[0]); + return R.ok(); + } +} diff --git a/src/main/java/com/cool/modules/plugin/entity/PluginInfoEntity.java b/src/main/java/com/cool/modules/plugin/entity/PluginInfoEntity.java new file mode 100644 index 0000000..5a9cbcb --- /dev/null +++ b/src/main/java/com/cool/modules/plugin/entity/PluginInfoEntity.java @@ -0,0 +1,68 @@ +package com.cool.modules.plugin.entity; + +import com.cool.core.base.BaseEntity; +import com.cool.core.config.PluginJson; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.handler.Fastjson2TypeHandler; +import com.tangzc.autotable.annotation.Ignore; +import com.tangzc.autotable.annotation.Index; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.tangzc.mybatisflex.autotable.annotation.UniIndex; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "plugin_info", comment = "插件信息") +public class PluginInfoEntity extends BaseEntity { + + @ColumnDefine(comment = "名称") + private String name; + + @ColumnDefine(comment = "简介") + private String description; + + @UniIndex + @ColumnDefine(comment = "实例对象") + private String key; + + @Index + @ColumnDefine(comment = "Hook", length = 50) + private String hook; + + @ColumnDefine(comment = "描述", type = "text") + private String readme; + + @ColumnDefine(comment = "版本") + private String version; + + @ColumnDefine(comment = "Logo(base64)", type = "text", notNull = true) + private String logo; + + @ColumnDefine(comment = "作者") + private String author; + + @ColumnDefine(comment = "状态 0-禁用 1-启用", defaultValue = "1") + private Integer status; + + @ColumnDefine(comment = "插件的plugin.json", type = "json", notNull = true) + @Column(typeHandler = Fastjson2TypeHandler.class) + private PluginJson pluginJson; + + @ColumnDefine(comment = "jar二进制文件", type = "longblob", notNull = true) + private byte[] jarFile; + + @ColumnDefine(comment = "配置", type = "json") + @Column(typeHandler = Fastjson2TypeHandler.class) + private Map config; + + @Ignore + @Column(ignore = true) + public String keyName; + + public String getKeyName() { + return key; + } +} diff --git a/src/main/java/com/cool/modules/plugin/mapper/PluginInfoMapper.java b/src/main/java/com/cool/modules/plugin/mapper/PluginInfoMapper.java new file mode 100644 index 0000000..c38e8d7 --- /dev/null +++ b/src/main/java/com/cool/modules/plugin/mapper/PluginInfoMapper.java @@ -0,0 +1,8 @@ +package com.cool.modules.plugin.mapper; + +import com.cool.modules.plugin.entity.PluginInfoEntity; +import com.mybatisflex.core.BaseMapper; + +public interface PluginInfoMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cool/modules/plugin/service/PluginInfoService.java b/src/main/java/com/cool/modules/plugin/service/PluginInfoService.java new file mode 100644 index 0000000..a8291d7 --- /dev/null +++ b/src/main/java/com/cool/modules/plugin/service/PluginInfoService.java @@ -0,0 +1,12 @@ +package com.cool.modules.plugin.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.plugin.entity.PluginInfoEntity; + +public interface PluginInfoService extends BaseService { + PluginInfoEntity getByKeyNoJarFile(String key); + + PluginInfoEntity getPluginInfoEntityByHookNoJarFile(String hook); + + PluginInfoEntity getPluginInfoEntityByIdNoJarFile(Long id); +} diff --git a/src/main/java/com/cool/modules/plugin/service/impl/PluginInfoServiceImpl.java b/src/main/java/com/cool/modules/plugin/service/impl/PluginInfoServiceImpl.java new file mode 100644 index 0000000..a18bcc8 --- /dev/null +++ b/src/main/java/com/cool/modules/plugin/service/impl/PluginInfoServiceImpl.java @@ -0,0 +1,55 @@ +package com.cool.modules.plugin.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.util.EntityUtils; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import com.cool.modules.plugin.mapper.PluginInfoMapper; +import com.cool.modules.plugin.service.PluginInfoService; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Service; + +import static com.cool.modules.plugin.entity.table.PluginInfoEntityTableDef.PLUGIN_INFO_ENTITY; + +/** + * 插件信息服务类 + */ +@Service +public class PluginInfoServiceImpl extends BaseServiceImpl + implements PluginInfoService { + + /** + * 通过key获取插件信息,不带jar二进制 + */ + @Override + public PluginInfoEntity getByKeyNoJarFile(String key) { + QueryWrapper queryWrapper = getPluginInfoEntityQueryWrapper(); + queryWrapper.and(PLUGIN_INFO_ENTITY.KEY.eq(key)); + return getOne(queryWrapper); + } + + /** + * 通过hook获取插件信息,不带jar二进制 + */ + @Override + public PluginInfoEntity getPluginInfoEntityByHookNoJarFile(String hook) { + QueryWrapper queryWrapper = getPluginInfoEntityQueryWrapper().and(PLUGIN_INFO_ENTITY.HOOK.eq(hook)) + .and(PLUGIN_INFO_ENTITY.STATUS.eq(1)).limit(1); + return getOne(queryWrapper); + } + + /** + * 通过id获取插件信息,不带jar二进制 + */ + @Override + public PluginInfoEntity getPluginInfoEntityByIdNoJarFile(Long id) { + QueryWrapper queryWrapper = getPluginInfoEntityQueryWrapper().and(PLUGIN_INFO_ENTITY.ID.eq(id)); + return getOne(queryWrapper); + } + + /** + * 获取查询对象,排除掉 jar二进制 + */ + private QueryWrapper getPluginInfoEntityQueryWrapper() { + return QueryWrapper.create().select(EntityUtils.getFieldNamesWithSuperClass(PLUGIN_INFO_ENTITY.DEFAULT_COLUMNS, PLUGIN_INFO_ENTITY.JAR_FILE.getName())); + } +} diff --git a/src/main/java/com/cool/modules/recycle/aop/AopConfig.java b/src/main/java/com/cool/modules/recycle/aop/AopConfig.java new file mode 100644 index 0000000..e44708d --- /dev/null +++ b/src/main/java/com/cool/modules/recycle/aop/AopConfig.java @@ -0,0 +1,15 @@ +//package com.cool.modules.recycle.aop; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.context.annotation.EnableAspectJAutoProxy; +// +//@Configuration +//@EnableAspectJAutoProxy +//public class AopConfig { +// +// @Bean +// public DeleteAspect deleteAspect() { +// return new DeleteAspect(); +// } +//} diff --git a/src/main/java/com/cool/modules/recycle/aop/DeleteAspect.java b/src/main/java/com/cool/modules/recycle/aop/DeleteAspect.java new file mode 100644 index 0000000..ad9f10f --- /dev/null +++ b/src/main/java/com/cool/modules/recycle/aop/DeleteAspect.java @@ -0,0 +1,114 @@ +package com.cool.modules.recycle.aop; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.IgnoreRecycleData; +import com.cool.core.base.BaseController; +import com.cool.core.base.BaseService; +import com.cool.modules.base.security.CoolSecurityUtil; +import com.cool.modules.recycle.entity.RecycleDataEntity; +import com.cool.modules.recycle.service.RecycleDataService; +import com.mybatisflex.core.query.QueryWrapper; +import jakarta.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +/** + * 数据删除前拦截 + */ +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class DeleteAspect { + + final private RecycleDataService recycleDataService; + + final private CoolSecurityUtil coolSecurityUtil; + + @Around(value = "execution(* com.cool.core.base.BaseController.delete*(..)) && args(request, params, requestParams)", argNames = "joinPoint,request,params,requestParams") + public Object aroundAdvice(ProceedingJoinPoint joinPoint, HttpServletRequest request, + Map params, + JSONObject requestParams) throws Throwable { + Method currentMethod = getCurrentMethod(joinPoint); + if (Objects.nonNull(currentMethod) && currentMethod.isAnnotationPresent( + IgnoreRecycleData.class)) { + // 忽略回收站记录 + return joinPoint.proceed(); + } + List list = null; + String className = null; + try { + log.info("数据删除前拦截"); + // 可以在目标方法执行前进行一些操作 + BaseController baseController = (BaseController) joinPoint.getTarget(); + BaseService service = baseController.getService(); + className = (baseController.currentEntityClass()).getName(); + QueryWrapper queryWrapper = new QueryWrapper(); + Object ids = params.get("ids"); + if (!(ids instanceof ArrayList)) { + ids = ids.toString().split(","); + } + List idList = Convert.toList(Long.class, ids); + queryWrapper.in("id", (Object) Convert.toLongArray(idList)); + list = service.list(queryWrapper); + } catch (Exception e) { + log.error("数据删除前拦截获取数据详情信息失败", e); + } + + Object result = joinPoint.proceed(); + if (ObjUtil.isNotEmpty(list)) { + RecycleDataEntity recycleDataEntity = new RecycleDataEntity(); + recycleDataEntity.setUrl(request.getRequestURI()); + recycleDataEntity.setUserName(coolSecurityUtil.username()); + recycleDataEntity + .setUserId(Long.parseLong( + String.valueOf(coolSecurityUtil.userInfo(requestParams).get("userId")))); + recycleDataEntity.setParams(params); + recycleDataEntity.setData(list); + recycleDataEntity.setParams(params); + RecycleDataEntity.EntityInfo entityInfo = new RecycleDataEntity.EntityInfo(); + entityInfo.setEntityClassName(className); + recycleDataEntity.setEntityInfo(entityInfo); + recycleDataEntity.setCount(recycleDataEntity.getData().size()); + + log.info("数据进入回收站 {}", recycleDataService.add(recycleDataEntity)); + } + return result; + } + + private Method getCurrentMethod(JoinPoint joinPoint) { + String methodName = joinPoint.getSignature().getName(); + Object[] args = joinPoint.getArgs(); + Method[] methods = joinPoint.getTarget().getClass().getMethods(); + + for (Method method : methods) { + if (method.getName().equals(methodName) && method.getParameterCount() == args.length) { + boolean isSameMethod = true; + Class[] parameterTypes = method.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + if (!parameterTypes[i].isAssignableFrom(args[i].getClass())) { + isSameMethod = false; + break; + } + } + if (isSameMethod) { + return method; + } + } + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/recycle/controller/admin/AdminRecycleDataController.java b/src/main/java/com/cool/modules/recycle/controller/admin/AdminRecycleDataController.java new file mode 100644 index 0000000..9e428e5 --- /dev/null +++ b/src/main/java/com/cool/modules/recycle/controller/admin/AdminRecycleDataController.java @@ -0,0 +1,40 @@ +package com.cool.modules.recycle.controller.admin; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.recycle.entity.RecycleDataEntity; +import com.cool.modules.recycle.entity.table.RecycleDataEntityTableDef; +import com.cool.modules.recycle.service.RecycleDataService; +import com.mybatisflex.core.query.QueryWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.Map; + + +/** + * 数据回收站 + */ +@Tag(name = "数据回收站", description = "数据回收站") +@CoolRestController(api = { "add", "delete", "update", "page", "list", "info" }) +@RequiredArgsConstructor +public class AdminRecycleDataController extends BaseController { + final private RecycleDataService recycleDataService; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setListOption(createOp().queryWrapper(QueryWrapper.create().orderBy(RecycleDataEntityTableDef.RECYCLE_DATA_ENTITY.CREATE_TIME, false))); + } + + @Operation(summary = "恢复数据", description = "恢复数据") + @PostMapping("/restore") + public R restore(@RequestBody Map params) { + return R.ok(this.service.restore(getIds(params))); + } +} diff --git a/src/main/java/com/cool/modules/recycle/entity/RecycleDataEntity.java b/src/main/java/com/cool/modules/recycle/entity/RecycleDataEntity.java new file mode 100644 index 0000000..0704e01 --- /dev/null +++ b/src/main/java/com/cool/modules/recycle/entity/RecycleDataEntity.java @@ -0,0 +1,56 @@ +package com.cool.modules.recycle.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.handler.Fastjson2TypeHandler; +import com.tangzc.autotable.annotation.Ignore; +import com.tangzc.autotable.annotation.Index; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +/** + * 数据回收站 软删除的时候数据会回收到该表 + */ +@Getter +@Setter +@Table(value = "recycle_data", comment = "数据回收站表") +public class RecycleDataEntity extends BaseEntity { + + @ColumnDefine(comment = "表信息", type = "json") + @Column(typeHandler = Fastjson2TypeHandler.class) + private EntityInfo entityInfo; + + @Index() + @ColumnDefine(comment = "操作人", notNull = true) + private Long userId; + + @ColumnDefine(comment = "被删除的数据", type = "json") + @Column(typeHandler = Fastjson2TypeHandler.class) + private List data; + + @ColumnDefine(comment = "请求的接口", notNull = true) + private String url; + + @ColumnDefine(comment = "请求参数", type = "json", notNull = true) + @Column(typeHandler = Fastjson2TypeHandler.class) + private Map params; + + @ColumnDefine(comment = "删除数据条数", defaultValue = "1") + private Integer count; + + @Setter + @Getter + public static class EntityInfo { + + // entityClassName + public String entityClassName; + } + + @Ignore + @Column(ignore = true) // 操作人名称 + public String userName; +} diff --git a/src/main/java/com/cool/modules/recycle/mapper/RecycleDataMapper.java b/src/main/java/com/cool/modules/recycle/mapper/RecycleDataMapper.java new file mode 100644 index 0000000..7fef289 --- /dev/null +++ b/src/main/java/com/cool/modules/recycle/mapper/RecycleDataMapper.java @@ -0,0 +1,7 @@ +package com.cool.modules.recycle.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.recycle.entity.RecycleDataEntity; + +public interface RecycleDataMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/recycle/service/RecycleDataService.java b/src/main/java/com/cool/modules/recycle/service/RecycleDataService.java new file mode 100644 index 0000000..612584e --- /dev/null +++ b/src/main/java/com/cool/modules/recycle/service/RecycleDataService.java @@ -0,0 +1,21 @@ +package com.cool.modules.recycle.service; + +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseService; +import com.cool.modules.recycle.entity.RecycleDataEntity; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; + +import java.util.List; + +public interface RecycleDataService extends BaseService { + Object page(JSONObject requestParams, Page page, QueryWrapper queryWrapper); + + /** + * 恢复数据 + * + * @param ids + * @return + */ + Boolean restore(List ids); +} diff --git a/src/main/java/com/cool/modules/recycle/service/impl/RecycleDataServiceImpl.java b/src/main/java/com/cool/modules/recycle/service/impl/RecycleDataServiceImpl.java new file mode 100644 index 0000000..4af9a84 --- /dev/null +++ b/src/main/java/com/cool/modules/recycle/service/impl/RecycleDataServiceImpl.java @@ -0,0 +1,118 @@ +package com.cool.modules.recycle.service.impl; + +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.base.service.MapperProviderService; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import com.cool.modules.base.service.sys.BaseSysUserService; +import com.cool.modules.recycle.entity.RecycleDataEntity; +import com.cool.modules.recycle.mapper.RecycleDataMapper; +import com.cool.modules.recycle.service.RecycleDataService; +import com.mybatisflex.core.BaseMapper; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 数据回收站 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RecycleDataServiceImpl extends BaseServiceImpl + implements RecycleDataService { + + final private BaseSysUserService baseSysUserService; + + final private MapperProviderService mapperProviderService; + + @Override + public Object page(JSONObject requestParams, Page page, + QueryWrapper queryWrapper) { + String keyWord = requestParams.getStr("keyWord"); + if (ObjUtil.isNotEmpty(keyWord)) { + List list = baseSysUserService + .list(queryWrapper.select(BaseSysUserEntity::getId) + .like(BaseSysUserEntity::getName, keyWord)) + .stream().map(BaseSysUserEntity::getId).toList(); + queryWrapper.like(RecycleDataEntity::getUrl, keyWord).or(w -> { + w.in(RecycleDataEntity::getUserId, list, ObjUtil.isNotEmpty(list)); + }); + } + Page iPage = page(page, queryWrapper); + List records = iPage.getRecords(); + List list = records.stream().map(RecycleDataEntity::getUserId) + .filter(ObjUtil::isNotEmpty).toList(); + + if (ObjUtil.isNotEmpty(list)) { + Map map = baseSysUserService + .list(QueryWrapper.create() + .select(BaseSysUserEntity::getId, BaseSysUserEntity::getName) + .in(BaseSysUserEntity::getId, list)) + .stream() + .collect(Collectors.toMap(BaseSysUserEntity::getId, BaseSysUserEntity::getName)); + records.forEach(o -> { + if (map.containsKey(o.getUserId())) { + o.setUserName(map.get(o.getUserId())); + } + }); + } + return iPage; + } + + @Override + public Boolean restore(List ids) { + if (ObjUtil.isEmpty(ids)) { + return false; + } + List list = list( + QueryWrapper.create().in(RecycleDataEntity::getId, ids)); + list.forEach(o -> { + // 处理恢复数据 + boolean flag = handlerRestore(o); + if (flag) { + // 删除回收站记录 + o.removeById(); + } + }); + return true; + } + + /** + * 处理数据恢复 + */ + private boolean handlerRestore(RecycleDataEntity recycleDataEntity) { + RecycleDataEntity.EntityInfo entityInfo = recycleDataEntity.getEntityInfo(); + try { + Class entityClass = ClassUtil.loadClass(entityInfo.getEntityClassName()); + List records = recycleDataEntity.getData(); + BaseMapper baseMapper = mapperProviderService.getMapperByEntityClass( + entityClass); + // 插入数据 + List insertList = new ArrayList<>(); + for (Object record : records) { + Object entity = JSONUtil.toBean(JSONUtil.parseObj(record), entityClass); + Method getIdMethod = entityClass.getMethod("getId"); + Object id = getIdMethod.invoke(entity); + if (baseMapper.selectOneById((Long) id) == null) { + insertList.add(entity); + } + } + baseMapper.insertBatch(insertList); + return true; + } catch (Exception e) { + log.error("恢复数据失败", e); + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceInfoController.java b/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceInfoController.java new file mode 100644 index 0000000..b1a35bf --- /dev/null +++ b/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceInfoController.java @@ -0,0 +1,24 @@ +package com.cool.modules.space.controller.admin; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.space.entity.SpaceInfoEntity; +import com.cool.modules.space.service.SpaceInfoService; +import io.swagger.v3.oas.annotations.tags.Tag; + +import jakarta.servlet.http.HttpServletRequest; + +import static com.cool.modules.space.entity.table.SpaceInfoEntityTableDef.SPACE_INFO_ENTITY; + +/** + * 文件空间信息 + */ +@Tag(name = "文件空间信息", description = "文件空间信息") +@CoolRestController(api = { "add", "delete", "update", "page", "list", "info" }) +public class AdminSpaceInfoController extends BaseController { + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption(createOp().fieldEq(SPACE_INFO_ENTITY.TYPE, SPACE_INFO_ENTITY.CLASSIFY_ID)); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceTypeController.java b/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceTypeController.java new file mode 100644 index 0000000..d5c0ace --- /dev/null +++ b/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceTypeController.java @@ -0,0 +1,23 @@ +package com.cool.modules.space.controller.admin; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.space.entity.SpaceTypeEntity; +import com.cool.modules.space.service.SpaceTypeService; +import io.swagger.v3.oas.annotations.tags.Tag; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 文件空间信息 + */ +@Tag(name = "文件空间信息", description = "文件空间信息") +@CoolRestController(api = { "add", "delete", "update", "page", "list", "info" }) +public class AdminSpaceTypeController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/space/entity/SpaceInfoEntity.java b/src/main/java/com/cool/modules/space/entity/SpaceInfoEntity.java new file mode 100644 index 0000000..596bc89 --- /dev/null +++ b/src/main/java/com/cool/modules/space/entity/SpaceInfoEntity.java @@ -0,0 +1,58 @@ +package com.cool.modules.space.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.tangzc.autotable.annotation.Ignore; +import com.tangzc.autotable.annotation.Index; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +/** + * 文件空间信息 + */ +@Getter +@Setter +@Table(value = "space_info", comment = "文件空间信息") +public class SpaceInfoEntity extends BaseEntity { + @ColumnDefine(comment = "地址", notNull = true) + private String url; + + @ColumnDefine(comment = "类型", notNull = true) + private String type; + + @ColumnDefine(comment = "分类ID", type = "tinyint") + private Integer classifyId; + + @Index() + @ColumnDefine(comment = "文件id") + private String fileId; + + @ColumnDefine(comment = "文件名") + private String name; + + @ColumnDefine(comment = "文件大小") + private Integer size; + + @ColumnDefine(comment = "文档版本", defaultValue = "1") + private Long version; + + @ColumnDefine(comment = "文件位置") + private String filePath; + + @Ignore + @Column(ignore = true) + private String key; + + public void setFilePath(String filePath) { + this.filePath = filePath; + this.key = filePath; + } + + public void setKey(String key) { + this.key = key; + this.filePath = key; + } +} diff --git a/src/main/java/com/cool/modules/space/entity/SpaceTypeEntity.java b/src/main/java/com/cool/modules/space/entity/SpaceTypeEntity.java new file mode 100644 index 0000000..ad8155d --- /dev/null +++ b/src/main/java/com/cool/modules/space/entity/SpaceTypeEntity.java @@ -0,0 +1,23 @@ +package com.cool.modules.space.entity; + +import com.cool.core.base.BaseEntity; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +/** + * 图片空间信息分类 + */ +@Getter +@Setter +@Table(value = "space_type", comment = "图片空间信息分类") +public class SpaceTypeEntity extends BaseEntity { + @ColumnDefine(comment = "类别名称", notNull = true) + private String name; + + @ColumnDefine(comment = "父分类ID", type = "tinyint") + private Integer parentId; +} diff --git a/src/main/java/com/cool/modules/space/mapper/SpaceInfoMapper.java b/src/main/java/com/cool/modules/space/mapper/SpaceInfoMapper.java new file mode 100644 index 0000000..3f41f26 --- /dev/null +++ b/src/main/java/com/cool/modules/space/mapper/SpaceInfoMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.space.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.space.entity.SpaceInfoEntity; + +/** + * 文件空间信息 + */ +public interface SpaceInfoMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/space/mapper/SpaceTypeMapper.java b/src/main/java/com/cool/modules/space/mapper/SpaceTypeMapper.java new file mode 100644 index 0000000..8858289 --- /dev/null +++ b/src/main/java/com/cool/modules/space/mapper/SpaceTypeMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.space.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.space.entity.SpaceTypeEntity; + +/** + * 文件空间信息 + */ +public interface SpaceTypeMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/space/service/SpaceInfoService.java b/src/main/java/com/cool/modules/space/service/SpaceInfoService.java new file mode 100644 index 0000000..bbf358e --- /dev/null +++ b/src/main/java/com/cool/modules/space/service/SpaceInfoService.java @@ -0,0 +1,10 @@ +package com.cool.modules.space.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.space.entity.SpaceInfoEntity; + +/** + * 文件空间信息 + */ +public interface SpaceInfoService extends BaseService { +} diff --git a/src/main/java/com/cool/modules/space/service/SpaceTypeService.java b/src/main/java/com/cool/modules/space/service/SpaceTypeService.java new file mode 100644 index 0000000..5f4aa5e --- /dev/null +++ b/src/main/java/com/cool/modules/space/service/SpaceTypeService.java @@ -0,0 +1,10 @@ +package com.cool.modules.space.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.space.entity.SpaceTypeEntity; + +/** + * 文件空间信息 + */ +public interface SpaceTypeService extends BaseService { +} diff --git a/src/main/java/com/cool/modules/space/service/impl/SpaceInfoServiceImpl.java b/src/main/java/com/cool/modules/space/service/impl/SpaceInfoServiceImpl.java new file mode 100644 index 0000000..de9ce6b --- /dev/null +++ b/src/main/java/com/cool/modules/space/service/impl/SpaceInfoServiceImpl.java @@ -0,0 +1,15 @@ +package com.cool.modules.space.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.space.entity.SpaceInfoEntity; +import com.cool.modules.space.mapper.SpaceInfoMapper; +import com.cool.modules.space.service.SpaceInfoService; +import org.springframework.stereotype.Service; + +/** + * 文件空间信息 + */ +@Service +public class SpaceInfoServiceImpl extends BaseServiceImpl + implements SpaceInfoService { +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/space/service/impl/SpaceTypeServiceImpl.java b/src/main/java/com/cool/modules/space/service/impl/SpaceTypeServiceImpl.java new file mode 100644 index 0000000..3b5894f --- /dev/null +++ b/src/main/java/com/cool/modules/space/service/impl/SpaceTypeServiceImpl.java @@ -0,0 +1,15 @@ +package com.cool.modules.space.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.space.entity.SpaceTypeEntity; +import com.cool.modules.space.mapper.SpaceTypeMapper; +import com.cool.modules.space.service.SpaceTypeService; +import org.springframework.stereotype.Service; + +/** + * 文件空间信息 + */ +@Service +public class SpaceTypeServiceImpl extends BaseServiceImpl + implements SpaceTypeService { +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/task/config/ScheduleConfig.java b/src/main/java/com/cool/modules/task/config/ScheduleConfig.java new file mode 100644 index 0000000..020ab47 --- /dev/null +++ b/src/main/java/com/cool/modules/task/config/ScheduleConfig.java @@ -0,0 +1,21 @@ +package com.cool.modules.task.config; + +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + + +/** + * 定时任务配置 + */ +@Configuration +@EnableScheduling +public class ScheduleConfig implements SchedulerFactoryBeanCustomizer { + @Override + public void customize(SchedulerFactoryBean schedulerFactoryBean) { + schedulerFactoryBean.setStartupDelay(2); + schedulerFactoryBean.setAutoStartup(true); + schedulerFactoryBean.setOverwriteExistingJobs(true); + } +} \ No newline at end of file diff --git a/src/main/java/com/cool/modules/task/controller/admin/AdminTaskInfoController.java b/src/main/java/com/cool/modules/task/controller/admin/AdminTaskInfoController.java new file mode 100644 index 0000000..ea0b070 --- /dev/null +++ b/src/main/java/com/cool/modules/task/controller/admin/AdminTaskInfoController.java @@ -0,0 +1,62 @@ +package com.cool.modules.task.controller.admin; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.task.entity.TaskInfoEntity; +import com.cool.modules.task.entity.table.TaskInfoEntityTableDef; +import com.cool.modules.task.service.TaskInfoService; +import com.mybatisflex.core.paginate.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; + +import jakarta.servlet.http.HttpServletRequest; + +import static com.cool.modules.task.entity.table.TaskInfoEntityTableDef.TASK_INFO_ENTITY; + +/** + * 任务 + */ +@Tag(name = "任务管理", description = "统一管理任务") +@CoolRestController(api = { "add", "delete", "update", "info", "page" }) +public class AdminTaskInfoController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption(createOp().fieldEq(TASK_INFO_ENTITY.STATUS, TASK_INFO_ENTITY.TYPE)); + } + + @Operation(summary = "执行一次") + @PostMapping("/once") + public R once(@RequestAttribute JSONObject requestParams) { + service.once(requestParams.getLong("id")); + return R.ok(); + } + + @Operation(summary = "开始任务") + @PostMapping("/start") + public R start(@RequestAttribute JSONObject requestParams) { + service.start(requestParams.getLong("id"), requestParams.getInt("type")); + return R.ok(); + } + + @Operation(summary = "停止任务") + @PostMapping("/stop") + public R stop(@RequestAttribute JSONObject requestParams) { + service.stop(requestParams.getLong("id")); + return R.ok(); + } + + @Operation(summary = "任务日志") + @GetMapping("/log") + public R log(@RequestAttribute JSONObject requestParams) { + Integer page = requestParams.getInt("page", 0); + Integer size = requestParams.getInt("size", 20); + return R.ok(pageResult((Page) service.log(new Page<>(page, size), requestParams.getLong("id"), + requestParams.getInt("status")))); + } +} diff --git a/src/main/java/com/cool/modules/task/entity/TaskInfoEntity.java b/src/main/java/com/cool/modules/task/entity/TaskInfoEntity.java new file mode 100644 index 0000000..91b9672 --- /dev/null +++ b/src/main/java/com/cool/modules/task/entity/TaskInfoEntity.java @@ -0,0 +1,64 @@ +package com.cool.modules.task.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; + +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +@Getter +@Setter +@Table(value = "task_info", comment = "任务信息") +public class TaskInfoEntity extends BaseEntity { + /** + * 任务调度参数key + */ + @Column(ignore = true) + public static final String JOB_PARAM_KEY = "JOB_PARAM_KEY"; + + @ColumnDefine(comment = "名称", notNull = true) + private String name; + + @ColumnDefine(comment = "任务ID") + private String jobId; + + @ColumnDefine(comment = "最大执行次数 不传为无限次") + private Integer repeatCount; + + @ColumnDefine(comment = "每间隔多少毫秒执行一次 如果cron设置了 这项设置就无效") + private Integer every; + + @ColumnDefine(comment = "状态 0:停止 1:运行", defaultValue = "1", notNull = true) + private Integer status; + + @ColumnDefine(comment = "服务实例名称") + private String service; + + @ColumnDefine(comment = "状态 0:cron 1:时间间隔", defaultValue = "0") + private Integer taskType; + + @ColumnDefine(comment = "状态 0:系统 1:用户", defaultValue = "0", type = "tinyint") + private Integer type; + + @ColumnDefine(comment = "任务数据") + private String data; + + @ColumnDefine(comment = "备注") + private String remark; + + @ColumnDefine(comment = "cron") + private String cron; + + @ColumnDefine(comment = "下一次执行时间") + private Date nextRunTime; + + @ColumnDefine(comment = "开始时间") + private Date startDate; + + @ColumnDefine(comment = "结束时间") + private Date endDate; +} diff --git a/src/main/java/com/cool/modules/task/entity/TaskLogEntity.java b/src/main/java/com/cool/modules/task/entity/TaskLogEntity.java new file mode 100644 index 0000000..a8057b2 --- /dev/null +++ b/src/main/java/com/cool/modules/task/entity/TaskLogEntity.java @@ -0,0 +1,30 @@ +package com.cool.modules.task.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.tangzc.autotable.annotation.Index; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "task_log", comment = "任务日志") +public class TaskLogEntity extends BaseEntity { + + @Index + @ColumnDefine(comment = "任务ID", notNull = true, type = "bigint") + private Long taskId; + + @ColumnDefine(comment = "状态 0:失败 1:成功", defaultValue = "0") + private Integer status; + + @ColumnDefine(comment = "详情", type = "text") + private String detail; + + // 任务名称 + @Column(ignore = true) + private String taskName; +} diff --git a/src/main/java/com/cool/modules/task/event/TaskEvent.java b/src/main/java/com/cool/modules/task/event/TaskEvent.java new file mode 100644 index 0000000..b43a638 --- /dev/null +++ b/src/main/java/com/cool/modules/task/event/TaskEvent.java @@ -0,0 +1,26 @@ +package com.cool.modules.task.event; + +import com.cool.core.init.DBFromJsonInit; +import com.cool.modules.task.service.TaskInfoService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 事件监听 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class TaskEvent { + + final private TaskInfoService taskInfoService; + + @EventListener + public void handleDbInitCompleteEvent(DBFromJsonInit.DbInitCompleteEvent event) { + taskInfoService.init(); + log.info("初始化任务"); + } +} diff --git a/src/main/java/com/cool/modules/task/mapper/TaskInfoMapper.java b/src/main/java/com/cool/modules/task/mapper/TaskInfoMapper.java new file mode 100644 index 0000000..770f7ea --- /dev/null +++ b/src/main/java/com/cool/modules/task/mapper/TaskInfoMapper.java @@ -0,0 +1,8 @@ +package com.cool.modules.task.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.task.entity.TaskInfoEntity; + +public interface TaskInfoMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cool/modules/task/mapper/TaskLogMapper.java b/src/main/java/com/cool/modules/task/mapper/TaskLogMapper.java new file mode 100644 index 0000000..10a0bce --- /dev/null +++ b/src/main/java/com/cool/modules/task/mapper/TaskLogMapper.java @@ -0,0 +1,8 @@ +package com.cool.modules.task.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.task.entity.TaskLogEntity; + +public interface TaskLogMapper extends BaseMapper { + +} diff --git a/src/main/java/com/cool/modules/task/run/ScheduleJob.java b/src/main/java/com/cool/modules/task/run/ScheduleJob.java new file mode 100644 index 0000000..fecd1b7 --- /dev/null +++ b/src/main/java/com/cool/modules/task/run/ScheduleJob.java @@ -0,0 +1,106 @@ +package com.cool.modules.task.run; + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.cool.modules.task.entity.TaskInfoEntity; +import com.cool.modules.task.entity.TaskLogEntity; +import com.cool.modules.task.service.TaskInfoLogService; +import com.cool.modules.task.service.TaskInfoService; +import com.mybatisflex.core.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.quartz.JobExecutionContext; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.scheduling.quartz.QuartzJobBean; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * 定时任务 + */ +@Slf4j +public class ScheduleJob extends QuartzJobBean { + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + @Override + protected void executeInternal(JobExecutionContext context) { + // 获取spring bean + TaskInfoLogService taskInfoLogService = SpringUtil.getBean(TaskInfoLogService.class); + // 获取spring bean + TaskInfoService taskInfoService = SpringUtil.getBean(TaskInfoService.class); + + Scheduler scheduler = SpringUtil.getBean(Scheduler.class); + + TaskInfoEntity taskInfoEntity = taskInfoService + .getById(context.getJobDetail().getKey().getName().split("_")[1]); + if (ObjUtil.isEmpty(taskInfoEntity)) { + log.warn("taskInfoEntity is null"); + return; + } + + // 数据库保存执行记录 + TaskLogEntity taskLogEntity = new TaskLogEntity(); + + // 任务开始时间 + long startTime = System.currentTimeMillis(); + + try { + // 执行任务 + log.info("任务准备执行,任务ID:" + taskInfoEntity.getJobId()); + taskLogEntity.setTaskId(taskInfoEntity.getId()); + // 解析执行 + String service = taskInfoEntity.getService(); + if (StrUtil.isNotEmpty(service)) { + String[] arr = service.split("\\."); + String methodName = arr[1].substring(0, arr[1].indexOf("(")); + String params = service.substring(service.indexOf("(") + 1, service.indexOf(")")); + + ScheduleRunnable task = new ScheduleRunnable(StringUtil.firstCharToLowerCase(arr[0]).replaceAll(" ", ""), methodName, params); + Future future = executorService.submit(task); + + future.get(); + } + // 任务执行总时长 + long times = System.currentTimeMillis() - startTime; + // 状态 0:失败 1:成功 + taskLogEntity.setStatus(1); + taskLogEntity.setDetail("任务执行完毕,任务ID:" + taskInfoEntity.getJobId() + " 总共耗时:" + times + "毫秒"); + log.info(taskLogEntity.getDetail()); + } catch (Exception e) { + // 任务执行总时长 + long times = System.currentTimeMillis() - startTime; + + taskLogEntity.setDetail( + "任务执行失败,任务ID:" + taskInfoEntity.getJobId() + " 总共耗时:" + times + "毫秒" + "失败原因:" + e.getMessage()); + log.error("任务执行失败,任务ID:" + taskInfoEntity.getJobId(), e); + + // 状态 0:失败 1:成功 + taskLogEntity.setStatus(0); + } finally { + taskInfoLogService.add(taskLogEntity); + } + ThreadUtil.execAsync(() -> { + ThreadUtil.sleep(2000); + TaskInfoEntity next = new TaskInfoEntity(); + next.setId(taskInfoEntity.getId()); + try { + if (!scheduler.checkExists(context.getTrigger().getJobKey())) { + if (context.getTrigger().getNextFireTime() == null) { + next.setStatus(0); + } + } else { + if (context.getTrigger().getNextFireTime() == null) { + next.setNextRunTime(context.getTrigger().getNextFireTime()); + } + } + } catch (SchedulerException e) { + log.error("err", e); + } + taskInfoService.updateById(next); + }); + } +} diff --git a/src/main/java/com/cool/modules/task/run/ScheduleRunnable.java b/src/main/java/com/cool/modules/task/run/ScheduleRunnable.java new file mode 100644 index 0000000..dbae43c --- /dev/null +++ b/src/main/java/com/cool/modules/task/run/ScheduleRunnable.java @@ -0,0 +1,101 @@ +package com.cool.modules.task.run; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.cool.core.exception.CoolException; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; + +/** + * 执行定时任务 + */ +public class ScheduleRunnable implements Runnable { + private final Object target; + private final Method method; + private final Object[] params; + + public ScheduleRunnable(String beanName, String methodName, String params) + throws NoSuchMethodException, SecurityException { + this.target = SpringUtil.getBean(beanName); + + if (StrUtil.isNotBlank(params)) { + String[] paramArray = params.split(","); + this.params = new Object[paramArray.length]; + Class[] paramTypes = new Class[paramArray.length]; + + for (int i = 0; i < paramArray.length; i++) { + String param = paramArray[i].trim(); + if (param.matches("-?\\d+")) { + this.params[i] = Integer.parseInt(param); + paramTypes[i] = int.class; + } else if (param.matches("-?\\d+L")) { + this.params[i] = Long.parseLong(param.substring(0, param.length() - 1)); + paramTypes[i] = long.class; + } else if (param.matches("-?\\d+\\.\\d+")) { + this.params[i] = Double.parseDouble(param); + paramTypes[i] = double.class; + } else if (param.matches("-?\\d+\\.\\d+f")) { + this.params[i] = Float.parseFloat(param.substring(0, param.length() - 1)); + paramTypes[i] = float.class; + } else if (param.equalsIgnoreCase("true") || param.equalsIgnoreCase("false")) { + this.params[i] = Boolean.parseBoolean(param); + paramTypes[i] = boolean.class; + } else if (param.length() == 1) { + this.params[i] = param.charAt(0); + paramTypes[i] = char.class; + } else { + // Remove leading and trailing quotation marks for string parameters + if (param.startsWith("\"") && param.endsWith("\"")) { + param = param.substring(1, param.length() - 1); + } + this.params[i] = param; + paramTypes[i] = String.class; + } + } + + this.method = findMethod(target.getClass(), methodName, paramTypes); + } else { + this.params = new Object[0]; + this.method = target.getClass().getDeclaredMethod(methodName); + } + } + + private Method findMethod(Class targetClass, String methodName, Class[] paramTypes) throws NoSuchMethodException { + try { + return targetClass.getDeclaredMethod(methodName, paramTypes); + } catch (NoSuchMethodException e) { + // Try with wrapper classes + for (int i = 0; i < paramTypes.length; i++) { + if (paramTypes[i] == int.class) { + paramTypes[i] = Integer.class; + } else if (paramTypes[i] == long.class) { + paramTypes[i] = Long.class; + } else if (paramTypes[i] == double.class) { + paramTypes[i] = Double.class; + } else if (paramTypes[i] == float.class) { + paramTypes[i] = Float.class; + } else if (paramTypes[i] == boolean.class) { + paramTypes[i] = Boolean.class; + } else if (paramTypes[i] == char.class) { + paramTypes[i] = Character.class; + } + } + return targetClass.getDeclaredMethod(methodName, paramTypes); + } + } + + @Override + public void run() { + try { + ReflectionUtils.makeAccessible(method); + if (params.length > 0) { + method.invoke(target, params); + } else { + method.invoke(target); + } + } catch (Exception e) { + throw new CoolException("执行定时任务失败", e); + } + } +} diff --git a/src/main/java/com/cool/modules/task/service/TaskInfoLogService.java b/src/main/java/com/cool/modules/task/service/TaskInfoLogService.java new file mode 100644 index 0000000..435c0a4 --- /dev/null +++ b/src/main/java/com/cool/modules/task/service/TaskInfoLogService.java @@ -0,0 +1,10 @@ +package com.cool.modules.task.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.task.entity.TaskLogEntity; + +/** + * 任务日志 + */ +public interface TaskInfoLogService extends BaseService { +} diff --git a/src/main/java/com/cool/modules/task/service/TaskInfoService.java b/src/main/java/com/cool/modules/task/service/TaskInfoService.java new file mode 100644 index 0000000..6f9657c --- /dev/null +++ b/src/main/java/com/cool/modules/task/service/TaskInfoService.java @@ -0,0 +1,48 @@ +package com.cool.modules.task.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.task.entity.TaskInfoEntity; +import com.cool.modules.task.entity.TaskLogEntity; +import com.mybatisflex.core.paginate.Page; + +/** + * 任务信息 + */ +public interface TaskInfoService extends BaseService { + + /** + * 初始化任务 + */ + void init(); + + /** + * 执行一次 + * + * @param taskId 任务ID + */ + void once(Long taskId); + + /** + * 停止任务 + * + * @param taskId 任务ID + */ + void stop(Long taskId); + + /** + * 任务日志 + * + * @param taskId 任务ID + * @param status 任务状态 + * @return 日志列表 + */ + Object log(Page page, Long taskId, Integer status); + + /** + * 开始任务 + * + * @param taskId 任务ID + * @param type 任务类型 + */ + void start(Long taskId, Integer type); +} diff --git a/src/main/java/com/cool/modules/task/service/impl/TaskInfoLogServiceImpl.java b/src/main/java/com/cool/modules/task/service/impl/TaskInfoLogServiceImpl.java new file mode 100644 index 0000000..d1f6ccd --- /dev/null +++ b/src/main/java/com/cool/modules/task/service/impl/TaskInfoLogServiceImpl.java @@ -0,0 +1,12 @@ +package com.cool.modules.task.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.task.entity.TaskLogEntity; +import com.cool.modules.task.mapper.TaskLogMapper; +import com.cool.modules.task.service.TaskInfoLogService; +import org.springframework.stereotype.Service; + +@Service +public class TaskInfoLogServiceImpl extends BaseServiceImpl + implements TaskInfoLogService { +} diff --git a/src/main/java/com/cool/modules/task/service/impl/TaskInfoServiceImpl.java b/src/main/java/com/cool/modules/task/service/impl/TaskInfoServiceImpl.java new file mode 100644 index 0000000..394db7d --- /dev/null +++ b/src/main/java/com/cool/modules/task/service/impl/TaskInfoServiceImpl.java @@ -0,0 +1,138 @@ +package com.cool.modules.task.service.impl; + +import static com.cool.modules.task.entity.table.TaskInfoEntityTableDef.TASK_INFO_ENTITY; +import static com.cool.modules.task.entity.table.TaskLogEntityTableDef.TASK_LOG_ENTITY; + +import cn.hutool.core.convert.Convert; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.task.entity.TaskInfoEntity; +import com.cool.modules.task.entity.TaskLogEntity; +import com.cool.modules.task.mapper.TaskInfoMapper; +import com.cool.modules.task.service.TaskInfoService; +import com.cool.modules.task.utils.ScheduleUtils; +import com.mybatisflex.core.paginate.Page; +import com.mybatisflex.core.query.QueryWrapper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.CronTrigger; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TaskInfoServiceImpl extends BaseServiceImpl implements + TaskInfoService { + + final private Scheduler scheduler; + + @Override + public void init() { + try { + List list = list(); + list.forEach(scheduleJob -> { + CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, + scheduleJob.getJobId()); + if (cronTrigger == null) { + ScheduleUtils.createScheduleJob(scheduler, scheduleJob); + } else { + ScheduleUtils.updateScheduleJob(scheduler, scheduleJob); + } + updateById(scheduleJob); + }); + } catch (Exception ignored) { + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void once(Long taskId) { + ScheduleUtils.run(scheduler, getById(taskId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void stop(Long taskId) { + ScheduleUtils.pauseJob(scheduler, taskId + ""); + TaskInfoEntity taskInfoEntity = getById(taskId); + taskInfoEntity.setStatus(0); + updateById(taskInfoEntity); + modifyAfter(JSONUtil.parseObj(taskInfoEntity), taskInfoEntity); + } + + @Override + public Object log(Page page, Long taskId, Integer status) { + + QueryWrapper queryWrapper = QueryWrapper.create().select(TASK_LOG_ENTITY.ALL_COLUMNS, + TASK_INFO_ENTITY.NAME.as("taskName")).from(TASK_LOG_ENTITY) + .leftJoin(TASK_INFO_ENTITY).on(TASK_LOG_ENTITY.TASK_ID.eq(TASK_INFO_ENTITY.ID)) + .eq(TaskLogEntity::getTaskId, taskId, taskId != null) + .eq(TaskLogEntity::getStatus, status, status != null) + .orderBy(TaskLogEntity::getCreateTime, false); + return mapper.paginate(page, queryWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void start(Long taskId, Integer type) { + TaskInfoEntity taskInfoEntity = getById(taskId); + taskInfoEntity.setStatus(1); + if (type != null) { + taskInfoEntity.setType(type); + } + boolean isExists = false; + try { + isExists = scheduler.checkExists(ScheduleUtils.getJobKey(taskId + "")); + } catch (SchedulerException e) { + log.error("err", e); + } + if (isExists) { + ScheduleUtils.updateScheduleJob(scheduler, taskInfoEntity); + ScheduleUtils.resumeJob(scheduler, taskId + ""); + } else { + ScheduleUtils.createScheduleJob(scheduler, taskInfoEntity); + } + updateById(taskInfoEntity); + modifyAfter(JSONUtil.parseObj(taskInfoEntity), taskInfoEntity); + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long add(JSONObject requestParams, TaskInfoEntity scheduleJob) { + scheduleJob.setStatus(1); + super.add(scheduleJob); + scheduleJob.setJobId(scheduleJob.getId() + ""); + + ScheduleUtils.createScheduleJob(scheduler, scheduleJob); + updateById(scheduleJob); + super.modifyAfter(requestParams, scheduleJob); + return scheduleJob.getId(); + } + + @Override + public boolean update(JSONObject requestParams, TaskInfoEntity entity) { + updateById(entity); + ScheduleUtils.deleteScheduleJob(scheduler, entity.getId().toString()); + if (entity.getStatus() == 1) { + start(entity.getId(), entity.getType()); + } else { + stop(entity.getId()); + } + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean delete(JSONObject requestParams, Long... ids) { + Convert.toList(String.class, ids).forEach(jobId -> { + ScheduleUtils.deleteScheduleJob(scheduler, jobId); + }); + return super.delete(requestParams, ids); + } +} diff --git a/src/main/java/com/cool/modules/task/utils/ScheduleUtils.java b/src/main/java/com/cool/modules/task/utils/ScheduleUtils.java new file mode 100644 index 0000000..7e82df5 --- /dev/null +++ b/src/main/java/com/cool/modules/task/utils/ScheduleUtils.java @@ -0,0 +1,251 @@ +package com.cool.modules.task.utils; + +import com.cool.core.exception.CoolException; +import com.cool.modules.task.entity.TaskInfoEntity; +import com.cool.modules.task.run.ScheduleJob; +import org.quartz.*; + +/** + * 定时任务工具类 + * + * @author Mark sunlightcs@gmail.com + * @since 1.2.0 2016-11-28 + */ +public class ScheduleUtils { + private final static String JOB_NAME = "TASK_"; + + public enum ScheduleStatus { + /** + * 暂停 + */ + PAUSE(0), + /** + * 正常 + */ + NORMAL(1); + + private int value; + + ScheduleStatus(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + /** + * 获取触发器key + */ + public static TriggerKey getTriggerKey(String jobId) { + return TriggerKey.triggerKey(JOB_NAME + jobId); + } + + /** + * 获取jobKey + */ + public static JobKey getJobKey(String jobId) { + return JobKey.jobKey(JOB_NAME + jobId); + } + + /** + * 获取表达式触发器 + */ + public static CronTrigger getCronTrigger(Scheduler scheduler, String jobId) { + try { + return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId)); + } catch (SchedulerException e) { + throw new CoolException("获取定时任务CronTrigger出现异常", e); + } + } + + /** + * 获取表达式触发器 + */ + public static SimpleTrigger getSimpleTrigger(Scheduler scheduler, String jobId) { + try { + return (SimpleTrigger) scheduler.getTrigger(getTriggerKey(jobId)); + } catch (SchedulerException e) { + throw new CoolException("获取定时任务CronTrigger出现异常", e); + } + } + + /** + * 创建定时任务 + */ + public static void createScheduleJob(Scheduler scheduler, TaskInfoEntity scheduleJob) { + try { + // 构建job信息 + JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class).withIdentity(getJobKey(scheduleJob.getJobId())) + .build(); + + if (scheduleJob.getTaskType() == 0) { + // 表达式调度构建器 + CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCron()) + .withMisfireHandlingInstructionDoNothing(); + + TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger() + .withIdentity(getTriggerKey(scheduleJob.getJobId())).withSchedule(scheduleBuilder); + + if (scheduleJob.getStartDate() != null) { + triggerBuilder.startAt(scheduleJob.getStartDate()); + } + + if (scheduleJob.getEndDate() != null) { + triggerBuilder.endAt(scheduleJob.getEndDate()); + } + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = triggerBuilder.build(); + + scheduler.scheduleJob(jobDetail, trigger); + scheduleJob.setNextRunTime(trigger.getNextFireTime()); + } + + if (scheduleJob.getTaskType() == 1) { + SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() + .withIntervalInSeconds(scheduleJob.getEvery() / 1000); + if (scheduleJob.getRepeatCount() != null) { + scheduleBuilder.withRepeatCount(scheduleJob.getRepeatCount()); + } else { + scheduleBuilder.repeatForever(); + } + TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger() + .withIdentity(getTriggerKey(scheduleJob.getJobId())).withSchedule(scheduleBuilder); + if (scheduleJob.getStartDate() != null) { + triggerBuilder.startAt(scheduleJob.getStartDate()); + } + + if (scheduleJob.getEndDate() != null) { + triggerBuilder.endAt(scheduleJob.getEndDate()); + } + Trigger trigger = triggerBuilder.build(); + + scheduler.scheduleJob(jobDetail, trigger); + scheduleJob.setNextRunTime(trigger.getNextFireTime()); + } + + // 暂停任务 + if (scheduleJob.getStatus() != null && scheduleJob.getStatus() == ScheduleStatus.PAUSE.getValue()) { + pauseJob(scheduler, scheduleJob.getJobId()); + } + } catch (SchedulerException e) { + throw new CoolException("创建定时任务失败", e); + } + } + + /** + * 更新定时任务 + */ + public static void updateScheduleJob(Scheduler scheduler, TaskInfoEntity scheduleJob) { + try { + TriggerKey triggerKey = getTriggerKey(scheduleJob.getJobId()); + + if (scheduleJob.getTaskType() == 0) { + // 表达式调度构建器 + CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCron()) + .withMisfireHandlingInstructionDoNothing(); + + CronTrigger trigger = getCronTrigger(scheduler, scheduleJob.getJobId()); + + TriggerBuilder triggerBuilder = trigger.getTriggerBuilder(); + + if (scheduleJob.getStartDate() != null) { + triggerBuilder.startAt(scheduleJob.getStartDate()); + } + + if (scheduleJob.getEndDate() != null) { + triggerBuilder.endAt(scheduleJob.getEndDate()); + } + + // 按新的cronExpression表达式重新构建trigger + trigger = triggerBuilder.withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); + + scheduler.rescheduleJob(triggerKey, trigger); + scheduleJob.setNextRunTime(trigger.getNextFireTime()); + + } + + if (scheduleJob.getTaskType() == 1) { + SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() + .withIntervalInSeconds(scheduleJob.getEvery() / 1000); + + SimpleTrigger trigger = getSimpleTrigger(scheduler, scheduleJob.getJobId()); + + if (scheduleJob.getRepeatCount() != null) { + scheduleBuilder.withRepeatCount(scheduleJob.getRepeatCount()); + } else { + scheduleBuilder.repeatForever(); + } + TriggerBuilder triggerBuilder = trigger.getTriggerBuilder(); + if (scheduleJob.getStartDate() != null) { + triggerBuilder.startAt(scheduleJob.getStartDate()); + } + + if (scheduleJob.getEndDate() != null) { + triggerBuilder.endAt(scheduleJob.getEndDate()); + } + trigger = triggerBuilder.withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); + + scheduler.rescheduleJob(triggerKey, trigger); + scheduleJob.setNextRunTime(trigger.getNextFireTime()); + } + + // 暂停任务 + if (scheduleJob.getStatus() == ScheduleStatus.PAUSE.getValue()) { + pauseJob(scheduler, scheduleJob.getJobId()); + } + + } catch (SchedulerException e) { + throw new CoolException("更新定时任务失败", e); + } + } + + /** + * 立即执行任务 + */ + public static void run(Scheduler scheduler, TaskInfoEntity scheduleJob) { + try { + // 参数 + JobDataMap dataMap = new JobDataMap(); + + scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap); + } catch (SchedulerException e) { + throw new CoolException("立即执行定时任务失败", e); + } + } + + /** + * 暂停任务 + */ + public static void pauseJob(Scheduler scheduler, String jobId) { + try { + scheduler.pauseJob(getJobKey(jobId)); + } catch (SchedulerException e) { + throw new CoolException("暂停定时任务失败", e); + } + } + + /** + * 恢复任务 + */ + public static void resumeJob(Scheduler scheduler, String jobId) { + try { + scheduler.resumeJob(getJobKey(jobId)); + } catch (SchedulerException e) { + throw new CoolException("恢复定时任务失败", e); + } + } + + /** + * 删除定时任务 + */ + public static void deleteScheduleJob(Scheduler scheduler, String jobId) { + try { + scheduler.deleteJob(getJobKey(jobId)); + } catch (SchedulerException e) { + throw new CoolException("删除定时任务失败", e); + } + } +} diff --git a/src/main/java/com/cool/modules/user/controller/admin/AdminUserInfoController.java b/src/main/java/com/cool/modules/user/controller/admin/AdminUserInfoController.java new file mode 100644 index 0000000..833762d --- /dev/null +++ b/src/main/java/com/cool/modules/user/controller/admin/AdminUserInfoController.java @@ -0,0 +1,24 @@ +package com.cool.modules.user.controller.admin; + +import static com.cool.modules.user.entity.table.UserInfoEntityTableDef.USER_INFO_ENTITY; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.user.entity.UserInfoEntity; +import com.cool.modules.user.service.UserInfoService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; + +@Tag(name = "用户信息", description = "用户信息") +@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}) +public class AdminUserInfoController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + + setPageOption(createOp().fieldEq(USER_INFO_ENTITY.STATUS, USER_INFO_ENTITY.GENDER, + USER_INFO_ENTITY.LOGIN_TYPE) + .keyWordLikeFields(USER_INFO_ENTITY.NICK_NAME, USER_INFO_ENTITY.PHONE)); + } +} diff --git a/src/main/java/com/cool/modules/user/entity/UserInfoEntity.java b/src/main/java/com/cool/modules/user/entity/UserInfoEntity.java new file mode 100644 index 0000000..2e778ad --- /dev/null +++ b/src/main/java/com/cool/modules/user/entity/UserInfoEntity.java @@ -0,0 +1,41 @@ +package com.cool.modules.user.entity; + +import com.cool.core.base.BaseEntity; +import com.tangzc.autotable.annotation.Index; +import com.tangzc.autotable.annotation.enums.IndexTypeEnum; +import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "user_info", comment = "用户信息") +public class UserInfoEntity extends BaseEntity { + + @Index(type = IndexTypeEnum.UNIQUE) + @ColumnDefine(comment = "登录唯一ID", notNull = true) + private String unionid; + + @ColumnDefine(comment = "头像", notNull = true) + private String avatarUrl; + + @ColumnDefine(comment = "昵称", notNull = true) + private String nickName; + + @Index + @ColumnDefine(comment = "手机号", notNull = true) + private String phone; + + @ColumnDefine(comment = "性别 0-未知 1-男 2-女", defaultValue = "0") + private String gender; + + @ColumnDefine(comment = "状态 0-禁用 1-正常 2-已注销", defaultValue = "1") + private String status; + + @ColumnDefine(comment = "登录方式 0-小程序 1-公众号 2-H5", defaultValue = "0") + private String loginType; + + @ColumnDefine(comment = "密码", notNull = true) + private String password; +} diff --git a/src/main/java/com/cool/modules/user/mapper/UserInfoMapper.java b/src/main/java/com/cool/modules/user/mapper/UserInfoMapper.java new file mode 100644 index 0000000..5b9f71d --- /dev/null +++ b/src/main/java/com/cool/modules/user/mapper/UserInfoMapper.java @@ -0,0 +1,7 @@ +package com.cool.modules.user.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.user.entity.UserInfoEntity; + +public interface UserInfoMapper extends BaseMapper { +} diff --git a/src/main/java/com/cool/modules/user/service/UserInfoService.java b/src/main/java/com/cool/modules/user/service/UserInfoService.java new file mode 100644 index 0000000..bf5e41d --- /dev/null +++ b/src/main/java/com/cool/modules/user/service/UserInfoService.java @@ -0,0 +1,7 @@ +package com.cool.modules.user.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.user.entity.UserInfoEntity; + +public interface UserInfoService extends BaseService { +} diff --git a/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java b/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java new file mode 100644 index 0000000..0835aad --- /dev/null +++ b/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java @@ -0,0 +1,13 @@ +package com.cool.modules.user.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.user.entity.UserInfoEntity; +import com.cool.modules.user.mapper.UserInfoMapper; +import com.cool.modules.user.service.UserInfoService; +import org.springframework.stereotype.Service; + +@Service +public class UserInfoServiceImpl extends BaseServiceImpl implements + UserInfoService { + +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..b01e42b --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,33 @@ +spring: + datasource: + url: jdbc:mysql://127.0.0.1:3306/cool?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8 + username: root + password: 123456 + driver-class-name: com.mysql.cj.jdbc.Driver + +# AutoTable配置,根据实体类自动生成表 +auto-table: + # 启用自动维护表功能 + enable: true + # 自动删除名称不匹配的索引 + autoDropIndex: true + # 建表的时候,父类的字段排序是在子类后面还是前面 + superInsertPosition: before + # 模型包路径 + model-package: com.cool.modules.*.entity.* + +# Cool相关配置 +cool: + # 初始化数据 + initData: true + +# 文档 +springdoc: + api-docs: + #是否开启文档功能 本地为了配合eps功能不可关闭 + enabled: true + +# 设置日志级别 +logging: + level: + com.cool: debug \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..7d4b34d --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,27 @@ +spring: + datasource: + url: jdbc:mysql://127.0.0.1:3306/cool?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8 + username: root + password: 123456 + driver-class-name: com.mysql.cj.jdbc.Driver + +# AutoTable配置,根据实体类自动生成表 +auto-table: + # 启用自动维护表功能 + enable: false + +# Cool相关配置 +cool: + # 初始化数据 + initData: false + +# 文档 +springdoc: + api-docs: + #是否开启文档功能 本地为了配合eps功能不可关闭 + enabled: true + +# 设置日志级别 +logging: + level: + com.cool: error \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..82e4842 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,134 @@ +server: + port: 8001 + servlet: + context-path: / + +spring: + application: + name: cool-admin-java + profiles: + active: @spring.active@ + thymeleaf: + cache: false + prefix: classpath:/templates/ + suffix: .html + mode: HTML + #返回时间格式化 + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + + servlet: + multipart: + enabled: true + max-file-size: 100MB + max-request-size: 100MB + # Web设置 + web: + resources: + add-mappings: true + static-locations: classpath:/static/,file:./assets/public/ + + # caffeine 缓存 + cache: + type: caffeine + file: assets/cache + + #redis 缓存 + # cache: + # type: redis + # data: + # redis: + # host: 127.0.0.1 + # port: 6379 + # database: 0 + # password: + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: always + autoStartup: true + #相关属性配置 + properties: + org: + quartz: + scheduler: + instanceName: CoolScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + clusterCheckinInterval: 10000 + useProperties: false + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 1000 + threadPriority: 9 + threadsInheritContextClassLoaderOfInitializingThread: true + +# 忽略鉴权url +ignored: + urls: + - / + - /upload/** + - /actuator/** + - /download/** + - /static/** + - /app/** + - /favicon.ico + - /v3/api-docs/** + - /swagger + - /swagger-ui/** + - /css/* + - /js/* + - /druid/** + - /admin/base/open/** + - /admin/base/comm/eps + - /testPlugin/** +# 文档 +springdoc: + api-docs: + #swagger后端请求地址 + path: /v3/api-docs + swagger-ui: + #自定义swagger前端请求路径,输入http://127.0.0.1:端口号/swagger会自动重定向到swagger页面 + path: /swagger + +mybatis-flex: + #多数据源 + # datasource: + #MyBatis 配置文件位置,如果有单独的 MyBatis 配置,需要将其路径配置到 configLocation 中 + # configuration: + #MyBatis Mapper 所对应的 XML 文件位置,如果在 Mapper 中有自定义的方法(XML 中有自定义的实现),需要进行该配置,指定 Mapper 所对应的 XML 文件位置 + mapper-locations: [ "classpath*:/mapper/**/*.xml" ] + type-aliases-package: com.cool.modules.*.entity.* + global-config: + print-banner: false + + +# Cool相关配置 +cool: + # 缓存名称 + cacheName: comm + # 插件安装位置 + pluginPath: assets/plugin + # token 相关配置 + token: + # 过期时间 单位:秒 半小时 + expire: 1800 + # 刷新token过期时间 单位:秒 7天 + refreshExpire: 604800 + # 文件上传相关 + file: + #上传模式 + mode: local + # 本地上传配置 + local: + # 文件访问地址 + base-url: http://127.0.0.1:${server.port}/upload + +# AutoTable配置,根据实体类自动生成表 +auto-table: + show-banner: false diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..677a3f9 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,7 @@ + ______ ___ ___ _____ _ ______ ____ ____ _____ ____ _____ + .' ___ | .' `. .' `.|_ _| V7.x / \ |_ _ `.|_ \ / _||_ _||_ \|_ _| +/ .' \_|/ .-. \/ .-. \ | | ______ / _ \ | | `. \ | \/ | | | | \ | | +| | | | | || | | | | | _|______|/ ___ \ | | | | | |\ /| | | | | |\ \| | +\ `.___.'\\ `-' /\ `-' /_| |__/ | _/ / \ \_ _| |_.' /_| |_\/_| |_ _| |_ _| |_\ |_ + `.____ .' `.___.' `.___.'|________| |____| |____||______.'|_____||_____||_____||_____|\____| +:: https://java.cool-admin.com :: \ No newline at end of file diff --git a/src/main/resources/cool/code/controller.th b/src/main/resources/cool/code/controller.th new file mode 100644 index 0000000..279301d --- /dev/null +++ b/src/main/resources/cool/code/controller.th @@ -0,0 +1,21 @@ +package com.cool.modules.[(${module})].controller.[(${type})][(${subModule}?'.'+${subModule}:'')]; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.[(${module})].entity[(${subModule}?'.'+${subModule}:'')].[(${entity})]Entity; +import com.cool.modules.[(${module})].service[(${subModule}?'.'+${subModule}:'')].[(${entity})]Service; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; + +/** + * [(${name})] + */ +@Tag(name = "[(${name})]", description = "[(${name})]") +@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}) +public class [(${upperType})][(${entity})]Controller extends BaseController<[(${entity})]Service, [(${entity})]Entity> { + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + + } +} \ No newline at end of file diff --git a/src/main/resources/cool/code/mapper/interface.th b/src/main/resources/cool/code/mapper/interface.th new file mode 100644 index 0000000..112e6ab --- /dev/null +++ b/src/main/resources/cool/code/mapper/interface.th @@ -0,0 +1,10 @@ +package com.cool.modules.[(${module})].mapper[(${subModule}?'.'+${subModule}:'')]; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.[(${module})].entity[(${subModule}?'.'+${subModule}:'')].[(${entity})]Entity; + +/** + * [(${name})] + */ +public interface [(${entity})]Mapper extends BaseMapper<[(${entity})]Entity> { +} diff --git a/src/main/resources/cool/code/service/impl.th b/src/main/resources/cool/code/service/impl.th new file mode 100644 index 0000000..1c31ea7 --- /dev/null +++ b/src/main/resources/cool/code/service/impl.th @@ -0,0 +1,14 @@ +package com.cool.modules.[(${module})].service[(${subModule}?'.'+${subModule}:'')].impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.[(${module})].entity[(${subModule}?'.'+${subModule}:'')].[(${entity})]Entity; +import com.cool.modules.[(${module})].mapper[(${subModule}?'.'+${subModule}:'')].[(${entity})]Mapper; +import com.cool.modules.[(${module})].service[(${subModule}?'.'+${subModule}:'')].[(${entity})]Service; +import org.springframework.stereotype.Service; + +/** + * [(${name})] + */ +@Service +public class [(${entity})]ServiceImpl extends BaseServiceImpl<[(${entity})]Mapper, [(${entity})]Entity> implements [(${entity})]Service { +} \ No newline at end of file diff --git a/src/main/resources/cool/code/service/interface.th b/src/main/resources/cool/code/service/interface.th new file mode 100644 index 0000000..6b365b0 --- /dev/null +++ b/src/main/resources/cool/code/service/interface.th @@ -0,0 +1,10 @@ +package com.cool.modules.[(${module})].service[(${subModule}?'.'+${subModule}:'')]; + +import com.cool.core.base.BaseService; +import com.cool.modules.[(${module})].entity[(${subModule}?'.'+${subModule}:'')].[(${entity})]Entity; + +/** + * [(${name})] + */ +public interface [(${entity})]Service extends BaseService<[(${entity})]Entity> { +} diff --git a/src/main/resources/cool/data/db/base.json b/src/main/resources/cool/data/db/base.json new file mode 100644 index 0000000..5698188 --- /dev/null +++ b/src/main/resources/cool/data/db/base.json @@ -0,0 +1,121 @@ +{ + "base_sys_param": [ + { + "keyName": "rich", + "name": "富文本参数", + "data": "

这是一个富文本

xxx

xxxxxxxxxx


", + "dataType": 1, + "remark": null + }, + { + "keyName": "json", + "name": "JSON参数", + "data": "{\n \"code\": 111233\n}", + "dataType": 0, + "remark": null + }, + { + "keyName": "file", + "name": "文件", + "data": "", + "dataType": 2, + "remark": null + }, + { + "keyName": "text", + "name": "测试", + "data": "这是一段字符串", + "dataType": 0, + "remark": null + } + ], + "base_sys_conf": [ + { + "cKey": "logKeep", + "cValue": "31" + }, + { + "cKey": "recycleKeep", + "cValue": "31" + } + ], + "base_sys_department": [ + { + "id": 1, + "name": "COOL", + "parentId": null, + "orderNum": 0 + }, + { + "id": 11, + "name": "开发", + "parentId": 12, + "orderNum": 2 + }, + { + "id": 12, + "name": "测试", + "parentId": 1, + "orderNum": 1 + }, + { + "id": 13, + "name": "游客", + "parentId": 1, + "orderNum": 3 + } + ], + "base_sys_role": [ + { + "id": 1, + "userId": "1", + "name": "超管", + "label": "admin", + "remark": "最高权限的角色", + "relevance": 1, + "menuIdList": null, + "departmentIdList": null + } + ], + "base_sys_user": [ + { + "id": 1, + "departmentId": 1, + "name": "超级管理员", + "username": "admin", + "password": "e10adc3949ba59abbe56e057f20f883e", + "passwordV": 7, + "nickName": "管理员", + "headImg": "https://cool-js.com/admin/headimg.jpg", + "phone": "18000000000", + "email": "team@cool-js.com", + "status": 1, + "remark": "拥有最高权限的用户", + "socketId": null + } + ], + "base_sys_user_role": [ + { + "userId": 1, + "roleId": 1 + } + ], + "task_info": [ + { + "id": 1, + "name": "清理日志", + "job_id": "1", + "repeat_count": null, + "every": 5000, + "status": 1, + "service": "baseSysLogServiceImpl.clear(false)", + "task_type": 0, + "type": null, + "data": null, + "remark": null, + "cron": "0 0 1 * * ?", + "start_date": null, + "end_date": null + } + ] +} \ No newline at end of file diff --git a/src/main/resources/cool/data/menu/menu.json b/src/main/resources/cool/data/menu/menu.json new file mode 100644 index 0000000..8a62f75 --- /dev/null +++ b/src/main/resources/cool/data/menu/menu.json @@ -0,0 +1,863 @@ +[ + { + "name": "系统管理", + "router": "/sys", + "perms": null, + "type": 0, + "icon": "icon-system", + "orderNum": 2, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "权限管理", + "router": null, + "perms": null, + "type": 0, + "icon": "icon-auth", + "orderNum": 1, + "viewPath": null, + "keepAlive": false, + "isShow": true, + "childMenus": [ + { + "name": "菜单列表", + "router": "/sys/menu", + "perms": null, + "type": 1, + "icon": "icon-menu", + "orderNum": 2, + "viewPath": "modules/base/views/menu/index.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "新增", + "router": null, + "perms": "base:sys:menu:add", + "type": 2, + "icon": null, + "orderNum": 1, + "viewPath": null, + "keepAlive": false, + "isShow": true, + "childMenus": [] + }, + { + "name": "删除", + "router": null, + "perms": "base:sys:menu:delete", + "type": 2, + "icon": null, + "orderNum": 2, + "viewPath": null, + "keepAlive": false, + "isShow": true, + "childMenus": [] + }, + { + "name": "查询", + "router": null, + "perms": "base:sys:menu:page,base:sys:menu:list,base:sys:menu:info", + "type": 2, + "icon": null, + "orderNum": 4, + "viewPath": null, + "keepAlive": false, + "isShow": true, + "childMenus": [] + }, + { + "name": "参数", + "router": "/test/aa", + "perms": null, + "type": 1, + "icon": "icon-goods", + "orderNum": 0, + "viewPath": "modules/base/views/info.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "编辑", + "router": null, + "perms": "base:sys:menu:info,base:sys:menu:update", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + }, + { + "name": "角色列表", + "router": "/sys/role", + "perms": null, + "type": 1, + "icon": "icon-dept", + "orderNum": 3, + "viewPath": "cool/modules/base/views/role.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "新增", + "router": null, + "perms": "base:sys:role:add", + "type": 2, + "icon": null, + "orderNum": 1, + "viewPath": null, + "keepAlive": false, + "isShow": true, + "childMenus": [] + }, + { + "name": "删除", + "router": null, + "perms": "base:sys:role:delete", + "type": 2, + "icon": null, + "orderNum": 2, + "viewPath": null, + "keepAlive": false, + "isShow": true, + "childMenus": [] + }, + { + "name": "修改", + "router": null, + "perms": "base:sys:role:update", + "type": 2, + "icon": null, + "orderNum": 3, + "viewPath": null, + "keepAlive": false, + "isShow": true, + "childMenus": [] + }, + { + "name": "查询", + "router": null, + "perms": "base:sys:role:page,base:sys:role:list,base:sys:role:info", + "type": 2, + "icon": null, + "orderNum": 4, + "viewPath": null, + "keepAlive": false, + "isShow": true, + "childMenus": [] + } + ] + }, + { + "name": "用户列表", + "router": "/sys/user", + "perms": null, + "type": 1, + "icon": "icon-user", + "orderNum": 0, + "viewPath": "modules/base/views/user/index.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "部门列表", + "router": null, + "perms": "base:sys:department:list", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "新增部门", + "router": null, + "perms": "base:sys:department:add", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "更新部门", + "router": null, + "perms": "base:sys:department:update", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "删除部门", + "router": null, + "perms": "base:sys:department:delete", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "部门排序", + "router": null, + "perms": "base:sys:department:order", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "用户转移", + "router": null, + "perms": "base:sys:user:move", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "新增", + "router": null, + "perms": "base:sys:user:add", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "删除", + "router": null, + "perms": "base:sys:user:delete", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "修改", + "router": null, + "perms": "base:sys:user:delete,base:sys:user:update", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "查询", + "router": null, + "perms": "base:sys:user:page,base:sys:user:list,base:sys:user:info", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + } + ] + }, + { + "name": "参数配置", + "router": null, + "perms": null, + "type": 0, + "icon": "icon-params", + "orderNum": 3, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "参数列表", + "router": "/sys/param", + "perms": null, + "type": 1, + "icon": "icon-menu", + "orderNum": 0, + "viewPath": "cool/modules/base/views/param.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "新增", + "router": null, + "perms": "base:sys:param:add", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "修改", + "router": null, + "perms": "base:sys:param:info,base:sys:param:update", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "删除", + "router": null, + "perms": "base:sys:param:delete", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "查看", + "router": null, + "perms": "base:sys:param:page,base:sys:param:list,base:sys:param:info", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + } + ] + }, + { + "name": "监控管理", + "router": null, + "perms": null, + "type": 0, + "icon": "icon-monitor", + "orderNum": 9, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "请求日志", + "router": "/sys/log", + "perms": null, + "type": 1, + "icon": "icon-log", + "orderNum": 1, + "viewPath": "cool/modules/base/views/log.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "权限", + "router": null, + "perms": "base:sys:log:page,base:sys:log:clear,base:sys:log:getKeep,base:sys:log:setKeep", + "type": 2, + "icon": null, + "orderNum": 1, + "viewPath": null, + "keepAlive": false, + "isShow": true, + "childMenus": [] + } + ] + } + ] + }, + { + "name": "任务管理", + "router": null, + "perms": null, + "type": 0, + "icon": "icon-activity", + "orderNum": 9, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "任务列表", + "router": "/task/list", + "perms": null, + "type": 1, + "icon": "icon-menu", + "orderNum": 0, + "viewPath": "modules/task/views/list.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "权限", + "router": null, + "perms": "task:info:page,task:info:list,task:info:info,task:info:add,task:info:delete,task:info:update,task:info:stop,task:info:start,task:info:once,task:info:log", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + } + ] + } + ] + }, + { + "name": "框架教程", + "router": "/tutorial", + "perms": null, + "type": 0, + "icon": "icon-task", + "orderNum": 98, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "文档官网", + "router": "/tutorial/doc", + "perms": null, + "type": 1, + "icon": "icon-log", + "orderNum": 0, + "viewPath": "https://admin.cool-js.com", + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "crud 示例", + "router": "/demo/crud", + "perms": null, + "type": 1, + "icon": "icon-favor", + "orderNum": 1, + "viewPath": "modules/demo/views/crud/index.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + }, + { + "name": "通用", + "router": null, + "perms": null, + "type": 0, + "icon": "icon-radioboxfill", + "orderNum": 99, + "viewPath": null, + "keepAlive": true, + "isShow": false, + "childMenus": [ + { + "name": "图片上传", + "router": null, + "perms": "space:info:page,space:info:list,space:info:info,space:info:add,space:info:delete,space:info:update,space:type:page,space:type:list,space:type:info,space:type:add,space:type:delete,space:type:update", + "type": 2, + "icon": null, + "orderNum": 1, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + }, + { + "name": "首页", + "router": "/", + "perms": null, + "type": 1, + "icon": null, + "orderNum": 0, + "viewPath": "modules/demo/views/home/index.vue", + "keepAlive": true, + "isShow": false, + "childMenus": [] + }, + { + "name": "数据管理", + "router": null, + "perms": null, + "type": 0, + "icon": "icon-data", + "orderNum": 7, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "字典管理", + "router": "/dict/list", + "perms": null, + "type": 1, + "icon": "icon-dict", + "orderNum": 3, + "viewPath": "modules/dict/views/list.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "删除", + "router": null, + "perms": "dict:info:delete", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "修改", + "router": null, + "perms": "dict:info:update,dict:info:info", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "获得字典数据", + "router": null, + "perms": "dict:info:data", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "单个信息", + "router": null, + "perms": "dict:info:info", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "列表查询", + "router": null, + "perms": "dict:info:list", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "分页查询", + "router": null, + "perms": "dict:info:page", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "新增", + "router": null, + "perms": "dict:info:add", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "组权限", + "router": null, + "perms": "dict:type:list,dict:type:update,dict:type:delete,dict:type:add", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + }, + { + "name": "数据回收站", + "router": "/recycle/data", + "perms": null, + "type": 1, + "icon": "icon-delete", + "orderNum": 6, + "viewPath": "modules/recycle/views/data.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "恢复数据", + "router": null, + "perms": "recycle:data:restore", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "单个信息", + "router": null, + "perms": "recycle:data:info", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "分页查询", + "router": null, + "perms": "recycle:data:page", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + }, + { + "name": "文件管理", + "router": "/upload/list", + "perms": null, + "type": 1, + "icon": "icon-log", + "orderNum": 5, + "viewPath": "modules/space/views/list.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "权限", + "router": null, + "perms": "space:type:delete,space:type:update,space:type:info,space:type:list,space:type:page,space:type:add,space:info:getConfig,space:info:delete,space:info:update,space:info:info,space:info:list,space:info:page,space:info:add", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + } + ] + }, + { + "name": "用户管理", + "router": null, + "perms": null, + "type": 0, + "icon": "icon-user", + "orderNum": 11, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "用户列表", + "router": "/user/list", + "perms": null, + "type": 1, + "icon": "icon-menu", + "orderNum": 1, + "viewPath": "modules/user/views/list.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "删除", + "router": null, + "perms": "user:info:delete", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "修改", + "router": null, + "perms": "user:info:update,user:info:info", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "单个信息", + "router": null, + "perms": "user:info:info", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "列表查询", + "router": null, + "perms": "user:info:list", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "分页查询", + "router": null, + "perms": "user:info:page", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "name": "新增", + "router": null, + "perms": "user:info:add", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + } + ] + }, + { + "name": "扩展管理", + "router": null, + "perms": null, + "type": 0, + "icon": "icon-favor", + "orderNum": 8, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "后端插件", + "router": "/helper/plugins/serve", + "perms": null, + "type": 1, + "icon": "icon-component", + "orderNum": 2, + "viewPath": "modules/helper/views/plugins/serve.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "权限", + "router": null, + "perms": "plugin:info:install,plugin:info:delete,plugin:info:update,plugin:info:page,plugin:info:info", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + }, + { + "name": "前端插件", + "router": "/helper/plugins/vue", + "perms": null, + "type": 1, + "icon": "icon-vue", + "orderNum": 1, + "viewPath": "modules/helper/views/plugins/vue.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + } +] \ No newline at end of file diff --git a/src/main/resources/static/css/welcome.css b/src/main/resources/static/css/welcome.css new file mode 100644 index 0000000..c838986 --- /dev/null +++ b/src/main/resources/static/css/welcome.css @@ -0,0 +1,89 @@ +body { + display: flex; + height: 100vh; + justify-content: center; + align-items: center; + text-align: center; + background: #222; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.footer-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + color: #6ee1f5; + padding: 10px 0 20px 0; + text-align: center; + opacity: 0; /* 开始时隐藏 */ + animation: fadeIn 5s forwards; /* 应用动画 */ +} + +.link { + color: #6ee1f5; +} + +.reveal { + position: relative; + display: flex; + color: #6ee1f5; + font-size: 2em; + font-family: Raleway, sans-serif; + letter-spacing: 3px; + text-transform: uppercase; + white-space: pre; +} +.reveal span { + opacity: 0; + transform: scale(0); + animation: fadeIn 2.4s forwards; +} +.reveal::before, .reveal::after { + position: absolute; + content: ""; + top: 0; + bottom: 0; + width: 2px; + height: 100%; + background: white; + opacity: 0; + transform: scale(0); +} +.reveal::before { + left: 50%; + animation: slideLeft 1.5s cubic-bezier(0.7, -0.6, 0.3, 1.5) forwards; +} +.reveal::after { + right: 50%; + animation: slideRight 1.5s cubic-bezier(0.7, -0.6, 0.3, 1.5) forwards; +} + +@keyframes fadeIn { + to { + opacity: 1; + transform: scale(1); + } +} +@keyframes slideLeft { + to { + left: -6%; + opacity: 1; + transform: scale(0.9); + } +} +@keyframes slideRight { + to { + right: -6%; + opacity: 1; + transform: scale(0.9); + } +} diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..c4d673d Binary files /dev/null and b/src/main/resources/static/favicon.ico differ diff --git a/src/main/resources/static/js/welcome.js b/src/main/resources/static/js/welcome.js new file mode 100644 index 0000000..eb9b697 --- /dev/null +++ b/src/main/resources/static/js/welcome.js @@ -0,0 +1,14 @@ +const duration = 0.8; +const delay = 0.3; +// eslint-disable-next-line no-undef +const revealText = document.querySelector('.reveal'); +const letters = revealText.textContent.split(''); +revealText.textContent = ''; +const middle = letters.filter(e => e !== ' ').length / 2; +letters.forEach((letter, i) => { + // eslint-disable-next-line no-undef + const span = document.createElement('span'); + span.textContent = letter; + span.style.animationDelay = `${delay + Math.abs(i - middle) * 0.1}s`; + revealText.append(span); +}); diff --git a/src/main/resources/templates/welcome.html b/src/main/resources/templates/welcome.html new file mode 100644 index 0000000..3445adb --- /dev/null +++ b/src/main/resources/templates/welcome.html @@ -0,0 +1,25 @@ + + + + + + + + COOL-ADMIN 一个很酷的后台管理系统开发框架 + + + + + +
HELLO COOL-ADMIN V7.x
+ + + + + + diff --git a/src/test/java/com/cool/CoolAdminJavaApplicationTests.java b/src/test/java/com/cool/CoolAdminJavaApplicationTests.java new file mode 100644 index 0000000..1d280e5 --- /dev/null +++ b/src/test/java/com/cool/CoolAdminJavaApplicationTests.java @@ -0,0 +1,13 @@ +package com.cool; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CoolAdminJavaApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/cool/CoolCodeGeneratorTest.java b/src/test/java/com/cool/CoolCodeGeneratorTest.java new file mode 100644 index 0000000..40e9456 --- /dev/null +++ b/src/test/java/com/cool/CoolCodeGeneratorTest.java @@ -0,0 +1,22 @@ +package com.cool; + +import com.cool.core.code.CodeGenerator; +import com.cool.core.code.CodeModel; +import com.cool.core.code.CodeTypeEnum; + +public class CoolCodeGeneratorTest { + public static void main(String[] args) { + CodeGenerator codeGenerator = new CodeGenerator(); + codeGenerator.init(); + + CodeModel codeModel = new CodeModel(); + codeModel.setType(CodeTypeEnum.ADMIN); + codeModel.setName("测试CURD"); + codeModel.setModule("demo"); +// codeModel.setEntity(DemoEntity.class); + + codeGenerator.controller(codeModel); + codeGenerator.mapper(codeModel); + codeGenerator.service(codeModel); + } +}