diff --git a/cool-admin-java/.gitignore b/cool-admin-java/.gitignore new file mode 100644 index 0000000..d5ae312 --- /dev/null +++ b/cool-admin-java/.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/cool-admin-java/Dockerfile b/cool-admin-java/Dockerfile new file mode 100644 index 0000000..1108bb7 --- /dev/null +++ b/cool-admin-java/Dockerfile @@ -0,0 +1,14 @@ +# 使用 GraalVM 17 作为基础镜像 +FROM ghcr.io/graalvm/graalvm-ce:latest + +# 设置容器内的工作目录 +WORKDIR /app + +# 将可执行的jar文件复制到容器内 +COPY target/cool-admin-8.0.0.jar /app/cool-admin-8.0.0.jar + +# 暴露Spring Boot应用程序运行的端口 +EXPOSE 8001 + +# 运行Spring Boot应用程序的命令 +ENTRYPOINT ["java", "-jar", "/app/cool-admin-8.0.0.jar", "--spring.profiles.active=prod"] diff --git a/cool-admin-java/LICENSE b/cool-admin-java/LICENSE new file mode 100644 index 0000000..5a6831d --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/README.md b/cool-admin-java/README.md new file mode 100644 index 0000000..02f0c7e --- /dev/null +++ b/cool-admin-java/README.md @@ -0,0 +1,103 @@ + +

+ 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://java.cool-admin.com/src/guide/ai.html) +- **流程编排**:通过拖拽编排方式,即可实现类似像智能客服这样的功能[详情](https://cool-js.com/plugin/118) +- **多租户**:支持多租户,采用全局动态注入查询条件[详情](https://java.cool-admin.com/src/guide/tenant.html) +- **多语言**:基于大模型自动翻译,无需更改原有代码[详情](https://java.cool-admin.com/src/guide/i18n.html) +- **模块化**:代码是模块化的,清晰明了,方便维护 +- **插件化**:插件化的设计,可以通过安装插件的方式扩展如:支付、短信、邮件等功能 +- **自动初始化**:数据自动化,无需再手动维护,启动时自动生成数据库表和表结构数据 +- **cool-admin-java-plus**: [详情](https://gitee.com/hlc4417/cool-admin-java-plus) +- ...... +![](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://cool-js.com/plugin/140](https://cool-js.com/plugin/140) +- Ai流程编排+知识库项目:[https://cool-js.com/plugin/118](https://cool-js.com/plugin/118) +- cool-admin-java-plus:https://gitee.com/hlc4417/cool-admin-java-plus + +## 演示 + +[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) + +或 + +[https://gitcode.com/cool_team/cool-admin-vue](https://gitcode.com/cool_team/cool-admin-vue) + +## 微信群 + +Admin Wechat + +## 运行 + +### 环境要求 + +- Java Graalvm 17+ +- Maven 3.6+ + +### 配置 + +修改数据库配置,配置文件位于`src/resources/application-local.yml` + +以 Mysql 为例,其他数据库适配中... + +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) diff --git a/cool-admin-java/docker-compose.yml b/cool-admin-java/docker-compose.yml new file mode 100644 index 0000000..7c9130b --- /dev/null +++ b/cool-admin-java/docker-compose.yml @@ -0,0 +1,43 @@ +# 本地数据库环境 +# 数据存放在当前目录下的 data里 +# 推荐使用安装了docker扩展的vscode打开目录 在本文件上右键可以快速启动,停止 +# 如不需要相关容器开机自启动,可注释掉 restart: always +# 如遇端口冲突 可调整ports下 :前面的端口号 +version: "3.9" + +services: + mysql: + image: mysql # 使用官方 MySQL 镜像,你可以根据需要选择版本 + environment: + MYSQL_ROOT_PASSWORD: "123456" # 设置 root 用户密码 + MYSQL_DATABASE: "cool" # 创建一个初始数据库 + networks: + - backend + ports: + - "3306:3306" # 将主机的 3306 端口映射到容器的 3306 端口 + volumes: + - mysql-data:/var/lib/mysql # 挂载数据卷以持久化数据 + + redis: + image: redis:latest + # command: --requirepass "12345678" # Uncomment if you need a password + restart: always + environment: + TZ: Asia/Shanghai # 指定时区 + volumes: + - ./data/redis/:/data/ + networks: + - backend + ports: + - 6379:6379 + + +networks: + backend: + driver: bridge + +volumes: + mysql-data: + driver: local + redis-data: + driver: local \ No newline at end of file diff --git a/cool-admin-java/pom.xml b/cool-admin-java/pom.xml new file mode 100644 index 0000000..f43c908 --- /dev/null +++ b/cool-admin-java/pom.xml @@ -0,0 +1,213 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.cool + cool-admin + 8.0.0 + cool-admin + cool admin for java + + 17 + 1.18.34 + 1.11.0 + 1.11.0.146 + 5.8.26 + 3.3.2 + 2.0.51 + 2.5.0 + 0.9.16 + 4.7.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-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 + + + org.postgresql + postgresql + + + 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} + + + + com.github.binarywang + weixin-java-miniapp + ${weixin-java.version} + + + com.github.binarywang + weixin-java-mp + ${weixin-java.version} + + + com.github.binarywang + weixin-java-pay + ${weixin-java.version} + + + + jakarta.servlet + jakarta.servlet-api + provided + + + com.fasterxml.jackson.core + jackson-databind + + + org.perf4j + perf4j + ${perf4j.version} + + + + + + + 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/cool-admin-java/src/main/java/com/cool/CoolApplication.java b/cool-admin-java/src/main/java/com/cool/CoolApplication.java new file mode 100644 index 0000000..133ff49 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/CoolApplication.java @@ -0,0 +1,76 @@ +package com.cool; + +import com.cool.core.util.PathUtils; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.dromara.autotable.springboot.EnableAutoTable; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * CoolApplication - 应用程序的主类 + * 该类配置并运行应用程序。 + */ +@Slf4j +@EnableAutoTable // 开启自动建表 +@EnableAsync // 开启异步处理 +@EnableScheduling // 开启定时任务 +@EnableCaching // 开启缓存 +@SpringBootApplication +@MapperScan("com.cool.**.mapper") // 扫描指定包中的MyBatis映射器 +public class CoolApplication { + + private static volatile ConfigurableApplicationContext context; + private static ClassLoader mainThreadClassLoader; + + public static void main(String[] args) { + mainThreadClassLoader = Thread.currentThread().getContextClassLoader(); + context = SpringApplication.run(CoolApplication.class, args); + } + + /** + * 通过关闭当前上下文并启动新上下文来重启应用程序。 + */ + public static void restart(List javaPathList) { + // 从当前上下文获取应用程序参数 + ApplicationArguments args = context.getBean(ApplicationArguments.class); + + // 创建新线程来重启应用程序 + Thread thread = new Thread(() -> { + try { + // 关闭当前应用程序上下文 + context.close(); + // 等待上下文完全关闭 + while (context.isActive()) { + Thread.sleep(100); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + // 加载动态生成的代码 + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + javaPathList.forEach(javaPath -> { + try { + classLoader.loadClass(PathUtils.getClassName(javaPath)); + } catch (ClassNotFoundException e) { + log.error("loadClassErr {}", javaPath, e); + } + }); + // 使用相同的参数运行Spring Boot应用程序并设置上下文 + context = SpringApplication.run(CoolApplication.class, args.getSourceArgs()); + }); + // 设置线程的上下文类加载器 + thread.setContextClassLoader(mainThreadClassLoader); + // 确保线程不是守护线程 + thread.setDaemon(false); + // 启动线程 + thread.start(); + } + +} diff --git a/cool-admin-java/src/main/java/com/cool/Welcome.java b/cool-admin-java/src/main/java/com/cool/Welcome.java new file mode 100644 index 0000000..e34071a --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/Welcome.java @@ -0,0 +1,17 @@ +package com.cool; + +import com.cool.core.annotation.TokenIgnore; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequiredArgsConstructor +public class Welcome { + + @RequestMapping("/") + @TokenIgnore + public String welcome() { + return "welcome"; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/annotation/ColumnDefine.java b/cool-admin-java/src/main/java/com/cool/core/annotation/ColumnDefine.java new file mode 100644 index 0000000..6843e68 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/annotation/ColumnDefine.java @@ -0,0 +1,80 @@ +package com.cool.core.annotation; + +import org.dromara.autotable.annotation.ColumnComment; +import org.dromara.autotable.annotation.ColumnDefault; +import org.dromara.autotable.annotation.ColumnNotNull; +import org.dromara.autotable.annotation.ColumnType; +import org.dromara.autotable.annotation.enums.DefaultValueEnum; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ColumnType +@ColumnNotNull +@ColumnDefault +@ColumnComment("") +public @interface ColumnDefine { + + /** + * 字段类型:不填默认使用属性的数据类型进行转换,转换失败的字段不会添加 + * + * @return 字段类型 + */ + @AliasFor(annotation = ColumnType.class, attribute = "value") + String type() default ""; + + /** + * 字段长度,默认是-1,小于0相当于null + * + * @return 默认字段长度 + */ + @AliasFor(annotation = ColumnType.class, attribute = "length") + int length() default -1; + + /** + * 小数点长度,默认是-1,小于0相当于null + * + * @return 小数点长度 + */ + @AliasFor(annotation = ColumnType.class, attribute = "decimalLength") + int decimalLength() default -1; + + /** + * 是否为可以为null,true是可以,false是不可以,默认为true + * + * @return 是否为可以为null,true是不可以,false是可以,默认为false + */ + @AliasFor(annotation = ColumnNotNull.class, attribute = "value") + boolean notNull() default false; + + /** + * 默认值,默认为null + * + * @return 默认值 + */ + @AliasFor(annotation = ColumnDefault.class, attribute = "type") + DefaultValueEnum defaultValueType() default DefaultValueEnum.UNDEFINED; + + /** + * 默认值,默认为null + * + * @return 默认值 + */ + @AliasFor(annotation = ColumnDefault.class, attribute = "value") + String defaultValue() default ""; + + /** + * 数据表字段备注 + * + * @return 默认值,默认为空 + */ + @AliasFor(annotation = ColumnComment.class, attribute = "value") + String comment() default ""; +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/annotation/CoolPlugin.java b/cool-admin-java/src/main/java/com/cool/core/annotation/CoolPlugin.java new file mode 100644 index 0000000..a010335 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/annotation/CoolRestController.java b/cool-admin-java/src/main/java/com/cool/core/annotation/CoolRestController.java new file mode 100644 index 0000000..c13be69 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/annotation/CoolRestController.java @@ -0,0 +1,34 @@ +package com.cool.core.annotation; + +import java.lang.annotation.*; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 自定义路由注解 + */ +@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 {}; + + /** + * 如前缀: /admin/goods/searchKeyword + * 没指定该字段 cname="searchKeyword", + * 按规则是解析为: /admin/goods/search/keyword + * 前端和node版本已经定义为 searchKeyword,没按规则解析,使用该字段自定义规则 进行兼容 + * com.cool.core.request.prefix.AutoPrefixUrlMapping#getCName(java.lang.Class, java.lang.String) + */ + String cname() default ""; +} diff --git a/cool-admin-java/src/main/java/com/cool/core/annotation/EpsField.java b/cool-admin-java/src/main/java/com/cool/core/annotation/EpsField.java new file mode 100644 index 0000000..1e59245 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/annotation/EpsField.java @@ -0,0 +1,14 @@ +package com.cool.core.annotation; + +import com.cool.core.enums.AdminComponentsEnum; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface EpsField { + String component() default AdminComponentsEnum.INPUT; +} diff --git a/cool-admin-java/src/main/java/com/cool/core/annotation/IgnoreRecycleData.java b/cool-admin-java/src/main/java/com/cool/core/annotation/IgnoreRecycleData.java new file mode 100644 index 0000000..6ceb893 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/annotation/NoRepeatSubmit.java b/cool-admin-java/src/main/java/com/cool/core/annotation/NoRepeatSubmit.java new file mode 100644 index 0000000..818c473 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/annotation/NoRepeatSubmit.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.METHOD) +public @interface NoRepeatSubmit { + long expireTime() default 2000; // 默认2秒过期时间,单位毫秒 +} diff --git a/cool-admin-java/src/main/java/com/cool/core/annotation/TokenIgnore.java b/cool-admin-java/src/main/java/com/cool/core/annotation/TokenIgnore.java new file mode 100644 index 0000000..b2d46aa --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/annotation/TokenIgnore.java @@ -0,0 +1,15 @@ +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; + +/** + * 忽略Token验证 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TokenIgnore { + String[] value() default {}; +} diff --git a/cool-admin-java/src/main/java/com/cool/core/aop/NoRepeatSubmitAspect.java b/cool-admin-java/src/main/java/com/cool/core/aop/NoRepeatSubmitAspect.java new file mode 100644 index 0000000..501b771 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/aop/NoRepeatSubmitAspect.java @@ -0,0 +1,39 @@ +package com.cool.core.aop; + +import com.cool.core.annotation.NoRepeatSubmit; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.lock.CoolLock; +import com.cool.core.util.CoolSecurityUtil; +import jakarta.servlet.http.HttpServletRequest; +import java.time.Duration; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Aspect +@Component +@RequiredArgsConstructor +public class NoRepeatSubmitAspect { + + private final CoolLock coolLock; + + @Around("@annotation(noRepeatSubmit)") + public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable { + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull( + RequestContextHolder.getRequestAttributes())).getRequest(); + String key = request.getRequestURI() + ":" + CoolSecurityUtil.getCurrentUserId(); + // 加锁 + CoolPreconditions.check(!coolLock.tryLock(key, Duration.ofMillis(noRepeatSubmit.expireTime())), "请勿重复操作"); + try { + return joinPoint.proceed(); + } finally { + // 移除锁 + coolLock.unlock(key); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/base/BaseController.java b/cool-admin-java/src/main/java/com/cool/core/base/BaseController.java new file mode 100644 index 0000000..f76ba45 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/base/BaseController.java @@ -0,0 +1,289 @@ +package com.cool.core.base; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +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.enums.QueryModeEnum; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.request.CrudOption; +import com.cool.core.request.PageResult; +import com.cool.core.request.R; +import com.mybatisflex.core.paginate.Page; +import io.swagger.v3.oas.annotations.Operation; +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 java.util.Objects; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +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; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * 控制层基类 + * + * @param + * @param + */ +public abstract class BaseController, T extends BaseEntity> { + + @Getter + @Autowired + protected S service; + protected Class entityClass; + + protected final String COOL_PAGE_OP = "COOL_PAGE_OP"; + protected final String COOL_LIST_OP = "COOL_LIST_OP"; + protected final String COOL_INFO_OP = "COOL_INFO_OP"; + + private final ThreadLocal> pageOption = new ThreadLocal<>(); + private final ThreadLocal> listOption = new ThreadLocal<>(); + private final ThreadLocal> infoOption = new ThreadLocal<>(); + private final ThreadLocal requestParams = new ThreadLocal<>(); + + @ModelAttribute + protected void preHandle(HttpServletRequest request, + @RequestAttribute JSONObject requestParams) { + String requestPath = ((ServletRequestAttributes) Objects.requireNonNull( + RequestContextHolder.getRequestAttributes())).getRequest().getRequestURI(); + if (!requestPath.endsWith("/page") && !requestPath.endsWith("/list") + && !requestPath.endsWith("/info")) { + // 非page或list不执行 + return; + } + this.pageOption.set(new CrudOption<>(requestParams)); + this.listOption.set(new CrudOption<>(requestParams)); + this.infoOption.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()); + request.setAttribute(COOL_INFO_OP, this.infoOption.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 setInfoOption(CrudOption infoOption) { + this.infoOption.set(infoOption); + } + + 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("id", service.addBatch(requestParams, array.toList(currentEntityClass())))); + } else { + return R.ok(Dict.create().set("id", + service.add(requestParams, requestParams.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.getById(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, + @RequestAttribute(COOL_INFO_OP) CrudOption option) { + invokerTransformParam(option, requestParams); + T info = (T) service.info(requestParams, id); + invokerTransformValue(option, info); + return R.ok(info); + } + + /** + * 列表查询 + * + * @param requestParams 请求参数 + */ + @Operation(summary = "查询", description = "查询多个信息") + @PostMapping("/list") + protected R> list(@RequestAttribute() JSONObject requestParams, + @RequestAttribute(COOL_LIST_OP) CrudOption option) { + invokerTransformParam(option, requestParams); + QueryModeEnum queryModeEnum = option.getQueryModeEnum(); + List list = (List) switch (queryModeEnum) { + case ENTITY_WITH_RELATIONS -> service.listWithRelations(requestParams, option.getQueryWrapper(currentEntityClass())); + case CUSTOM -> transformList(service.list(requestParams, option.getQueryWrapper(currentEntityClass()), option.getAsType()), option.getAsType()); + default -> service.list(requestParams, option.getQueryWrapper(currentEntityClass())); + }; + invokerTransformValue(option, list); + return R.ok(list); + } + + /** + * 分页查询 + * + * @param requestParams 请求参数 + */ + @Operation(summary = "分页", description = "分页查询多个信息") + @PostMapping("/page") + protected R> page(@RequestAttribute() JSONObject requestParams, + @RequestAttribute(COOL_PAGE_OP) CrudOption option) { + invokerTransformParam(option, requestParams); + Integer page = requestParams.getInt("page", 1); + Integer size = requestParams.getInt("size", 20); + QueryModeEnum queryModeEnum = option.getQueryModeEnum(); + Object obj = switch (queryModeEnum) { + case ENTITY_WITH_RELATIONS -> service.pageWithRelations(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass())); + case CUSTOM -> transformPage(service.page(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass()), option.getAsType()), option.getAsType()); + default -> service.page(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass())); + }; + Page pageResult = (Page) obj; + invokerTransformValue(option, pageResult.getRecords()); + return R.ok(pageResult(pageResult)); + } + + /** + * 转换值,组装数据 + */ + private void invokerTransformValue(CrudOption option, Object obj) { + if (ObjUtil.isNotNull(option.getTransformValue())) { + if (obj instanceof List) { + ((List)obj).forEach(o -> { + option.getTransformValue().apply(o); + }); + } else { + option.getTransformValue().apply(obj); + } + } + } + + /** + * 转换入参 + */ + private void invokerTransformParam(CrudOption option, JSONObject obj) { + if (ObjUtil.isNotNull(option.getTransformParam())) { + option.getTransformParam().apply(obj); + } + } + + /** + * 分页结果 + * + * @param page 分页返回数据 + */ + protected PageResult pageResult(Page page) { + return PageResult.of(page); + } + + public Class currentEntityClass() { + if (entityClass != null) { + return this.entityClass; + } + // 使用 获取泛型参数类型 + Type type = TypeUtil.getTypeArgument(this.getClass(), 1); // 获取第二个泛型参数 + if (type instanceof Class) { + entityClass = (Class) type; + return entityClass; + } + 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); + } + + /** + * 适用于自定义返回值为 map,map 的key为数据库字段,转驼峰命名 + */ + protected List transformList(List records, Class asType) { + if (ObjUtil.isEmpty(asType) || !Map.class.isAssignableFrom(asType)) { + return records; + } + List list = new ArrayList<>(); + Editor keyEditor = property -> StrUtil.toCamelCase(property); + records.forEach(o -> + list.add(BeanUtil.beanToMap(o, new HashMap(), false, keyEditor))); + return list; + } + protected Page transformPage(Page page, Class asType) { + page.setRecords(transformList(page.getRecords(), asType)); + return page; + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/base/BaseEntity.java b/cool-admin-java/src/main/java/com/cool/core/base/BaseEntity.java new file mode 100644 index 0000000..fe88a73 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/base/BaseEntity.java @@ -0,0 +1,38 @@ +package com.cool.core.base; + +import com.fasterxml.jackson.annotation.JsonIgnore; +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.cool.core.annotation.ColumnDefine; +import java.io.Serializable; +import java.util.Date; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Ignore; + +/** + * 基础实体类 + */ +@Getter +@Setter +public abstract class BaseEntity> extends Model implements Serializable { + + @Id(keyType = KeyType.Auto, comment = "ID") + protected Long id; + + @Column(onInsertValue = "now()") + @ColumnDefine(comment = "创建时间") + protected Date createTime; + + @Column(onInsertValue = "now()", onUpdateValue = "now()") + @ColumnDefine(comment = "更新时间") + protected Date updateTime; + + @Ignore + @Column(ignore = true) + @JsonIgnore + private QueryWrapper queryWrapper; +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/base/BaseService.java b/cool-admin-java/src/main/java/com/cool/core/base/BaseService.java new file mode 100644 index 0000000..dff2fda --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/base/BaseService.java @@ -0,0 +1,175 @@ +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 queryWrapper 查询条件 + * @return 列表信息 + */ + List list(JSONObject requestParams, QueryWrapper queryWrapper, Class asType); + + /** + * 查询所有 + * 带关联查询 + * @param requestParams 请求参数 + * @param queryWrapper 查询条件 + * @return 列表信息 + */ + Object listWithRelations(JSONObject requestParams, QueryWrapper queryWrapper); + + /** + * 分页查询 + * + * @param requestParams 请求参数 + * @param page 分页信息 + * @param queryWrapper 查询条件 + * @return 分页信息 + */ + Object page(JSONObject requestParams, Page page, QueryWrapper queryWrapper); + + /** + * 分页查询 + * + * @param requestParams 请求参数 + * @param page 分页信息 + * @param queryWrapper 查询条件 + * @return 分页信息 + */ + Page page(JSONObject requestParams, Page page, QueryWrapper queryWrapper, Class asType); + + /** + * 分页查询 + * 带关联查询 + * @param requestParams 请求参数 + * @param page 分页信息 + * @param queryWrapper 查询条件 + * @return 分页信息 + */ + Object pageWithRelations(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); + + /** + * 修改之前 + * + * @param requestParams 请求参数 + * @param t 对应实体 + */ + void modifyBefore(JSONObject requestParams, T t); + + /** + * 修改之前 + * + * @param requestParams 请求参数 + * @param t 对应实体 + * @param type 修改类型 + */ + void modifyBefore(JSONObject requestParams, T t, ModifyEnum type); +} diff --git a/cool-admin-java/src/main/java/com/cool/core/base/BaseServiceImpl.java b/cool-admin-java/src/main/java/com/cool/core/base/BaseServiceImpl.java new file mode 100644 index 0000000..f4380cc --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/base/BaseServiceImpl.java @@ -0,0 +1,137 @@ +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.insertSelective(entity); + return entity.getId(); + } + + @Override + public Object add(JSONObject requestParams, T entity) { + this.modifyBefore(requestParams, entity, ModifyEnum.ADD); + this.add(entity); + this.modifyAfter(requestParams, entity, ModifyEnum.ADD); + return entity.getId(); + } + + @Override + public Object addBatch(JSONObject requestParams, List entitys) { + this.modifyBefore(requestParams, null, ModifyEnum.ADD); + 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) { + this.modifyBefore(requestParams, null, ModifyEnum.DELETE); + 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) { + this.modifyBefore(requestParams, entity, ModifyEnum.UPDATE); + 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 List list(JSONObject requestParams, QueryWrapper queryWrapper, Class asType) { + return mapper.selectListByQueryAs(queryWrapper, asType); + } + + @Override + public Object listWithRelations(JSONObject requestParams, QueryWrapper queryWrapper) { + return mapper.selectListWithRelationsByQuery(queryWrapper); + } + + @Override + public Object page(JSONObject requestParams, Page page, QueryWrapper queryWrapper) { + return this.page(page, queryWrapper); + } + + @Override + public Page page(JSONObject requestParams, Page page, QueryWrapper queryWrapper, + Class asType) { + return mapper.paginateAs(page, queryWrapper, asType); + } + + @Override + public Object pageWithRelations(JSONObject requestParams, Page page, + QueryWrapper queryWrapper) { + return mapper.paginateWithRelations(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); + } + + @Override + public void modifyBefore(JSONObject requestParams, T t) { + + } + + @Override + public void modifyBefore(JSONObject requestParams, T t, ModifyEnum type) { + + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/base/ModifyEnum.java b/cool-admin-java/src/main/java/com/cool/core/base/ModifyEnum.java new file mode 100644 index 0000000..22b0d85 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/base/TenantEntity.java b/cool-admin-java/src/main/java/com/cool/core/base/TenantEntity.java new file mode 100644 index 0000000..7938011 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/base/TenantEntity.java @@ -0,0 +1,16 @@ +package com.cool.core.base; + +import com.mybatisflex.core.activerecord.Model; +import com.cool.core.annotation.ColumnDefine; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; + +/** 租户ID实体类 */ +@Getter +@Setter +public class TenantEntity> extends BaseEntity { + @Index + @ColumnDefine(comment = "租户id") + protected Long tenantId; +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/base/service/MapperProviderService.java b/cool-admin-java/src/main/java/com/cool/core/base/service/MapperProviderService.java new file mode 100644 index 0000000..8492857 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/cache/CoolCache.java b/cool-admin-java/src/main/java/com/cool/core/cache/CoolCache.java new file mode 100644 index 0000000..aa6d381 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/cache/CoolCache.java @@ -0,0 +1,180 @@ +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.time.Duration; +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(); + } + } + + /** + * 数据来源 + */ + public interface ToCacheData { + Object apply(); + } + + /** + * 删除缓存 + * + * @param keys 一个或多个key + */ + public void del(String... keys) { + if (type.equalsIgnoreCase(CacheType.CAFFEINE.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; + } + + /** + * 普通缓存获取 + * + * @param key 键 + */ + public Object get(String key, Duration duration, ToCacheData toCacheData) { + Object ifNullValue = getIfNullValue(key); + if (ObjUtil.equals(ifNullValue, NULL_VALUE)) { + return null; + } + if (ObjUtil.isEmpty(ifNullValue)) { + Object obj = toCacheData.apply(); + set(key, obj, duration.toSeconds()); + return obj; + } + return ifNullValue; + } + + private Object getIfNullValue(String key) { + if (type.equalsIgnoreCase(CacheType.CAFFEINE.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())) { + // 放入缓存 + cache.put(key, value); + } else if (type.equalsIgnoreCase(CacheType.REDIS.name())) { + redisCache.put(cacheName, key.getBytes(), ObjectUtil.serialize(value), + java.time.Duration.ofSeconds(ttl)); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/code/CodeGenerator.java b/cool-admin-java/src/main/java/com/cool/core/code/CodeGenerator.java new file mode 100644 index 0000000..37a8400 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/code/CodeModel.java b/cool-admin-java/src/main/java/com/cool/core/code/CodeModel.java new file mode 100644 index 0000000..a2566fd --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/code/CodeTypeEnum.java b/cool-admin-java/src/main/java/com/cool/core/code/CodeTypeEnum.java new file mode 100644 index 0000000..873ef49 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/config/CoolProperties.java b/cool-admin-java/src/main/java/com/cool/core/config/CoolProperties.java new file mode 100644 index 0000000..88e3fe9 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/config/CustomOpenApiResource.java b/cool-admin-java/src/main/java/com/cool/core/config/CustomOpenApiResource.java new file mode 100644 index 0000000..e01d325 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/config/CustomOpenApiResource.java @@ -0,0 +1,40 @@ +package com.cool.core.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Locale; +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.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * 自定义 OpenApiResource + */ +@Component +@ConditionalOnProperty( + name = "springdoc.api-docs.enabled", + havingValue = "true" +) +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/cool-admin-java/src/main/java/com/cool/core/config/FileModeEnum.java b/cool-admin-java/src/main/java/com/cool/core/config/FileModeEnum.java new file mode 100644 index 0000000..243520b --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/config/FileProperties.java b/cool-admin-java/src/main/java/com/cool/core/config/FileProperties.java new file mode 100644 index 0000000..4af01c0 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/config/JacksonConfig.java b/cool-admin-java/src/main/java/com/cool/core/config/JacksonConfig.java new file mode 100644 index 0000000..8351306 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/config/JacksonConfig.java @@ -0,0 +1,67 @@ +package com.cool.core.config; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.NumberSerializer; +import java.io.IOException; +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; + +@Configuration +public class JacksonConfig { + + @Bean + public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() { + final Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); + final ObjectMapper objectMapper = builder.build(); + SimpleModule simpleModule = new SimpleModule(); + // Long,BigInteger 转为 String 防止 js 丢失精度 + simpleModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE); + simpleModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE); + simpleModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE); + objectMapper.registerModule(simpleModule); + // 配置日期格式为 yyyy-MM-dd HH:mm:ss + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + objectMapper.setDateFormat(dateFormat); + return new MappingJackson2HttpMessageConverter(objectMapper); + } + + /** + * 超出 JS 最大最小值 处理 + */ + @JacksonStdImpl + public static class BigNumberSerializer extends NumberSerializer { + + /** + * 根据 JS Number.MAX_SAFE_INTEGER 与 Number.MIN_SAFE_INTEGER 得来 + */ + private static final long MAX_SAFE_INTEGER = 9007199254740991L; + private static final long MIN_SAFE_INTEGER = -9007199254740991L; + + /** + * 提供实例 + */ + public static final BigNumberSerializer INSTANCE = new BigNumberSerializer(Number.class); + + public BigNumberSerializer(Class rawType) { + super(rawType); + } + + @Override + public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException { + // 超出范围 序列化位字符串 + if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { + super.serialize(value, gen, provider); + } else { + gen.writeString(value.toString()); + } + } + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/config/LocalFileProperties.java b/cool-admin-java/src/main/java/com/cool/core/config/LocalFileProperties.java new file mode 100644 index 0000000..5ecad6f --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/config/LocalFileProperties.java @@ -0,0 +1,30 @@ +package com.cool.core.config; + +import com.cool.core.util.PathUtils; +import java.io.File; +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() { + if (!PathUtils.isAbsolutePath(uploadPath)) { + // 相对路径 + return System.getProperty("user.dir") + File.separator + uploadPath; + } + // 绝对路径 + return uploadPath; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/config/LogDiscardPolicy.java b/cool-admin-java/src/main/java/com/cool/core/config/LogDiscardPolicy.java new file mode 100644 index 0000000..a661aaa --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/config/LogDiscardPolicy.java @@ -0,0 +1,13 @@ +package com.cool.core.config; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class LogDiscardPolicy implements RejectedExecutionHandler { + + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + log.warn("logTaskExecutor 当前已超过线程池最大队列容量,拒绝策略为丢弃该线程 {}", r.toString()); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/config/LogProperties.java b/cool-admin-java/src/main/java/com/cool/core/config/LogProperties.java new file mode 100644 index 0000000..0452ff8 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/config/LogProperties.java @@ -0,0 +1,28 @@ +package com.cool.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "cool.log") +public class LogProperties { + + /** + * 请求参数最大字节,超过请求参数不记录 + */ + private int maxByteLength; + /** + * 核心线程数的倍数 + */ + private int corePoolSizeMultiplier; + /** + * 最大线程数的倍数 + */ + private int maxPoolSizeMultiplier; + /** + * 队列容量的倍数 + */ + private int queueCapacityMultiplier; +} diff --git a/cool-admin-java/src/main/java/com/cool/core/config/MyBatisFlexConfiguration.java b/cool-admin-java/src/main/java/com/cool/core/config/MyBatisFlexConfiguration.java new file mode 100644 index 0000000..e7dcfa1 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/config/MyBatisFlexConfiguration.java @@ -0,0 +1,27 @@ +package com.cool.core.config; + +import com.cool.core.tenant.CoolTenantFactory; +import com.mybatisflex.core.FlexGlobalConfig; +import com.mybatisflex.core.tenant.TenantFactory; +import com.mybatisflex.spring.boot.MyBatisFlexCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MyBatisFlexConfiguration implements MyBatisFlexCustomizer { + + @Override + public void customize(FlexGlobalConfig globalConfig) { + // 我们可以在这里进行一些列的初始化配置 + + // 指定多租户列的列名 + FlexGlobalConfig.getDefaultConfig().setTenantColumn("tenant_id"); + } + + @Bean + @ConditionalOnProperty(name = "cool.multi-tenant.enable", havingValue = "true") + public TenantFactory tenantFactory(){ + return new CoolTenantFactory(); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/config/OssFileProperties.java b/cool-admin-java/src/main/java/com/cool/core/config/OssFileProperties.java new file mode 100644 index 0000000..1f02b3d --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/config/PluginJson.java b/cool-admin-java/src/main/java/com/cool/core/config/PluginJson.java new file mode 100644 index 0000000..ad39263 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/config/SwaggerConfig.java b/cool-admin-java/src/main/java/com/cool/core/config/SwaggerConfig.java new file mode 100644 index 0000000..8772d16 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/config/ThreadPoolConfig.java b/cool-admin-java/src/main/java/com/cool/core/config/ThreadPoolConfig.java new file mode 100644 index 0000000..9fc6dc7 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/config/ThreadPoolConfig.java @@ -0,0 +1,41 @@ +package com.cool.core.config; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +@RequiredArgsConstructor +public class ThreadPoolConfig { + + private final LogProperties logProperties; + + @Bean(name = "logTaskExecutor") + public Executor loggingTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + + int corePoolSize = Runtime.getRuntime().availableProcessors() * logProperties.getCorePoolSizeMultiplier(); + int maxPoolSize = corePoolSize * logProperties.getMaxPoolSizeMultiplier(); + int queueCapacity = maxPoolSize * logProperties.getQueueCapacityMultiplier(); + + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setThreadNamePrefix("logTask-"); + + // 自定义拒绝策略 + executor.setRejectedExecutionHandler(new LogDiscardPolicy()); + executor.initialize(); + return executor; + } + + @Bean(name = "cachedThreadPool") + public ExecutorService cachedThreadPool() { + // 创建一个虚拟线程池,每个任务使用一个虚拟线程执行 + return Executors.newCachedThreadPool(); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/config/TokenProperties.java b/cool-admin-java/src/main/java/com/cool/core/config/TokenProperties.java new file mode 100644 index 0000000..12cd713 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/config/cache/CaffeineConfig.java b/cool-admin-java/src/main/java/com/cool/core/config/cache/CaffeineConfig.java new file mode 100644 index 0000000..d22204c --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/config/cache/RedisConfig.java b/cool-admin-java/src/main/java/com/cool/core/config/cache/RedisConfig.java new file mode 100644 index 0000000..9da37da --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/enums/AdminComponentsEnum.java b/cool-admin-java/src/main/java/com/cool/core/enums/AdminComponentsEnum.java new file mode 100644 index 0000000..e4c95f1 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/enums/AdminComponentsEnum.java @@ -0,0 +1,105 @@ +package com.cool.core.enums; + +public class AdminComponentsEnum { + + + /** + * 省市区选择器 - 用户选择省市区 + */ + public static final String PCA = "pca"; + + /** + * 文本输入 - 文本编辑框 + */ + public static final String INPUT = "input"; + + /** + * 文本域 - 多行文本编辑框 + */ + public static final String TEXTAREA = "textarea"; + + /** + * 富文本编辑器 - 用于文章,商品详情的编辑 + */ + public static final String EDITOR_RICH = "editor-rich"; + + /** + * 代码编辑器 - 用于开发人员编写代码,支持多种语言,支持代码高亮,支持代码格式化 + */ + public static final String CODING = "coding"; + + /** + * 数字输入 - 数字输入编辑框 + */ + public static final String INPUT_NUMBER = "input-number"; + + /** + * 日期选择器 - 用户选择 年-月-日 + */ + public static final String DATE = "date"; + + /** + * 日期范围选择器 - 用户选择起始 年-月-日 + */ + public static final String DATERANGE = "daterange"; + + /** + * 时间选择器 - 用户选择 时:分:秒 + */ + public static final String DATETIME = "datetime"; + + /** + * 时间范围选择器 - 用户选择起始 年-月-日 时:分:秒 + */ + public static final String DATETIMERANGE = "datetimerange"; + + /** + * 单图上传 - 用户上传单张图片,如:头像、logo、封面 + */ + public static final String UPLOAD_IMG = "upload-img"; + + /** + * 多图上传 - 用户上传多张图片, 如:照片、图片 + */ + public static final String UPLOAD_IMG_MULTIPLE = "upload-img-multiple"; + + /** + * 单个文件上传 - 用户上传单个文件 + */ + public static final String UPLOAD_FILE = "upload-file"; + + /** + * 多个文件上传 - 用户上传多个文件 + */ + public static final String UPLOAD_FILE_MULTIPLE = "upload-file-multiple"; + + /** + * 状态选择器 - 用户开启或者关闭操作,如:是否启用、是否推荐、是否默认、置顶、启用禁用等 + */ + public static final String SWITCH = "switch"; + + /** + * 评分选择器 - 用户评分 + */ + public static final String RATE = "rate"; + + /** + * 滑块选择器 - 在一个固定区间内进行选择, 如:进度 + */ + public static final String PROGRESS = "progress"; + + /** + * 单选框 - 在一组备选项中进行单选,如:审批状态 + */ + public static final String RADIO = "radio"; + + /** + * 多选框 - 适用于选项比较少的情况,在一组备选项中进行多选, 如:学历、爱好等 + */ + public static final String CHECKBOX = "checkbox"; + + /** + * 下拉框 - 适用于当选项过多时,使用下拉菜单展示并选择内容,如:分类、标签等 + */ + public static final String SELECT = "select"; +} diff --git a/cool-admin-java/src/main/java/com/cool/core/enums/Apis.java b/cool-admin-java/src/main/java/com/cool/core/enums/Apis.java new file mode 100644 index 0000000..b8a4509 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/enums/Apis.java @@ -0,0 +1,13 @@ +package com.cool.core.enums; + +public class Apis { + public static final String ADD = "add"; + public static final String DELETE = "delete"; + public static final String UPDATE = "update"; + public static final String PAGE = "page"; + public static final String LIST = "list"; + public static final String INFO = "info"; + + + public static final String[] ALL_API = new String[]{ ADD, DELETE, UPDATE, PAGE, LIST, INFO }; +} diff --git a/cool-admin-java/src/main/java/com/cool/core/enums/QueryModeEnum.java b/cool-admin-java/src/main/java/com/cool/core/enums/QueryModeEnum.java new file mode 100644 index 0000000..8ab1e25 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/enums/QueryModeEnum.java @@ -0,0 +1,10 @@ +package com.cool.core.enums; + +/** + * 查询模式决定返回值 + */ +public enum QueryModeEnum { + ENTITY, // 实体(默认) + ENTITY_WITH_RELATIONS, // 实体关联查询(如实体字段上加 @RelationOneToMany 等注解) + CUSTOM , // 自定义,默认为Map +} diff --git a/cool-admin-java/src/main/java/com/cool/core/enums/UserTypeEnum.java b/cool-admin-java/src/main/java/com/cool/core/enums/UserTypeEnum.java new file mode 100644 index 0000000..a35be7c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/enums/UserTypeEnum.java @@ -0,0 +1,10 @@ +package com.cool.core.enums; + +/** + * 用户类型 + */ +public enum UserTypeEnum { + ADMIN, // 后台 + APP, // app + UNKNOWN, // 未知 +} diff --git a/cool-admin-java/src/main/java/com/cool/core/eps/CoolEps.java b/cool-admin-java/src/main/java/com/cool/core/eps/CoolEps.java new file mode 100644 index 0000000..5af862b --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/eps/CoolEps.java @@ -0,0 +1,410 @@ +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.ObjUtil; +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.annotation.EpsField; +import com.cool.core.annotation.TokenIgnore; +import com.cool.core.config.CustomOpenApiResource; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.util.*; +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 Dict menuInfo; + + private JSONObject swaggerInfo; + + @Value("${springdoc.api-docs.enabled:false}") + private boolean apiDocsEnabled; + + public Dict admin; + + public Dict app; + + final private RequestMappingHandlerMapping requestMappingHandlerMapping; + + @Async + public void init() { + if (!apiDocsEnabled) { + log.info("服务启动成功,端口:{}", serverPort); + return; + } + entityInfo = Dict.create(); + menuInfo = Dict.create(); + swaggerInfo = swaggerInfo(); + Runnable task = () -> { + entity(); + urls(); + log.info("初始化eps完成,服务启动成功,端口:{}", serverPort); + }; + // ThreadUtil.safeSleep(3000); + ThreadUtil.execute(task); + } + + /** + * 清空所有的数据 + */ + public void clear() { + admin = Dict.create(); + app = Dict.create(); + } + + /** + * 构建所有的url + */ + private void urls() { + Dict admin = Dict.create(); + Dict app = Dict.create(); + ArrayList emptyList = new ArrayList<>(); + Map map = requestMappingHandlerMapping.getHandlerMethods(); + for (Map.Entry methodEntry : map.entrySet()) { + RequestMappingInfo info = methodEntry.getKey(); + HandlerMethod method = methodEntry.getValue(); + TokenIgnore tokenIgnore = method.getMethodAnnotation(TokenIgnore.class); + String module = getModule(method); + if (StrUtil.isNotEmpty(module)) { + String entityName = getEntity(method.getBeanType()); + String methodPath = getMethodUrl(method); + String escapedMethodPath = methodPath.replace("{", "\\{").replace("}", "\\}"); + String prefix = Objects.requireNonNull(getUrl(info)) + .replaceFirst("(?s)(.*)" + escapedMethodPath, "$1"); + Dict result; + int type = 0; + if (prefix.startsWith("/admin")) { + result = admin; + } else if (prefix.startsWith("/app")) { + result = app; + type = 1; + } else { + continue; + } + 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"), tokenIgnore)); + } else { + item = Dict.create(); + item.set("controller", method.getBeanType().getSimpleName()); + item.set("module", module); + item.set("info", Dict.create().set("type", + Dict.create() + .set("name", getLastPathSegment(prefix)) + .set("description", "") + )); + item.set("api", apis(prefix, methodPath, item.getBean("api"), tokenIgnore)); + item.set("name", entityName); + item.set("columns", entityInfo.get(entityName)); + item.set("pageQueryOp", Dict.create().set("keyWordLikeFields", emptyList) + .set("fieldEq", emptyList) + .set("fieldLike", emptyList)); + item.set("prefix", prefix); + item.set("menu", menuInfo.get(entityName)); + urls.add(item); + } + if (type == 0) { + admin.set(module, urls); + } + if (type == 1) { + app.set(module, urls); + } + + } + } + this.admin = admin; + this.app = app; + + } + /** + * 提取URL路径中的最后一个路径段 + * 示例:输入 "/api/getData" 返回 "getData" + */ + private String getLastPathSegment(String url) { + if (StrUtil.isBlank(url)) { + return ""; + } + + int queryIndex = url.indexOf('?'); + if (queryIndex != -1) { + url = url.substring(0, queryIndex); + } + + int slashIndex = url.lastIndexOf('/'); + if (slashIndex != -1 && slashIndex < url.length() - 1) { + return url.substring(slashIndex + 1); + } else { + return url; + } + } + + + /** + * 设置所有的api + * + * @param prefix 路由前缀 + * @param methodPath 方法路由 + * @param list api列表 + * @return api列表 + */ + private List apis(String prefix, String methodPath, List list, TokenIgnore tokenIgnore) { + if (ObjUtil.isNull(list)) { + list = new ArrayList<>(); + } + Dict item = Dict.create(); + item.set("path", methodPath); + item.set("tag", ""); + item.set("dts", Dict.create()); + item.set("ignoreToken", false); + if (tokenIgnore != null) { + item.set("ignoreToken", true); + } + 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("method", method); + item.set("summary", methodInfo.getStr("summary")); + } + + /** + * 获得方法的url地址 + * + * @param handlerMethod 方法 + * @return 方法url地址 + */ + private String getMethodUrl(HandlerMethod handlerMethod) { + String url = ""; + 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") && ObjUtil.isNotEmpty(attributes.get("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 = SpringUtil.getBean(CustomOpenApiResource.class).getOpenApiJson(); + return JSONUtil.parseObj(new String(bytes)); + } catch (Exception e) { + return new JSONObject(); + } + } + + /** + * 获得Controller上的实体类型 + * + * @param controller Controller类 + * @return 实体名称 + */ + private String getEntity(Class controller) { + try { + ParameterizedType parameterizedType = (ParameterizedType) controller.getGenericSuperclass(); + Class entityClass = (Class) parameterizedType.getActualTypeArguments()[1]; + return entityClass.getSimpleName(); + } catch (Exception e) { + return ""; + } + } + + private void entity() { + // 扫描所有的实体类 + Set> classes = ClassUtil.scanPackageByAnnotation("", Table.class); + classes.forEach(e -> { + // 获得属性 + Field[] fields = getAllDeclaredFields(e); + List columns = columns(fields); + entityInfo.set(e.getSimpleName(), columns); + + + Table mergedAnnotation = AnnotatedElementUtils.findMergedAnnotation(e, Table.class); + + menuInfo.set(e.getSimpleName(), mergedAnnotation.comment()); + + + }); + } + + /** + * 获取类及其所有父类中声明的字段 + * + * @param clazz 要检查的类 + * @return 包含类及其所有父类中声明的所有字段的数组 + */ + public static Field[] getAllDeclaredFields(Class clazz) { + // 参数校验 + if (clazz == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + + List allFields = new ArrayList<>(); + Class currentClass = clazz; + + // 循环遍历类及其父类 + while (currentClass != null) { + Field[] declaredFields = currentClass.getDeclaredFields(); + allFields.addAll(Arrays.asList(declaredFields)); + currentClass = currentClass.getSuperclass(); + } + + // 将列表转换为数组返回 + return allFields.toArray(new Field[0]); + } + + /** + * 获得所有的列 + * + * @param fields 字段名 + * @return 所有的列 + */ + private List columns(Field[] fields) { + List dictList = new ArrayList<>(); + for (Field field : fields) { + Dict dict = Dict.create(); + + EpsField epsField = AnnotatedElementUtils.findMergedAnnotation(field, EpsField.class); + if (epsField != null) { + dict.set("component", epsField.component()); + } + + 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()); + dict.set("source", "a." + field.getName()); + 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/cool-admin-java/src/main/java/com/cool/core/eps/EpsEvent.java b/cool-admin-java/src/main/java/com/cool/core/eps/EpsEvent.java new file mode 100644 index 0000000..1e40b01 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/exception/CoolException.java b/cool-admin-java/src/main/java/com/cool/core/exception/CoolException.java new file mode 100644 index 0000000..90498a5 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/exception/CoolExceptionHandler.java b/cool-admin-java/src/main/java/com/cool/core/exception/CoolExceptionHandler.java new file mode 100644 index 0000000..01dec57 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/exception/CoolExceptionHandler.java @@ -0,0 +1,71 @@ +package com.cool.core.exception; + +import cn.hutool.core.util.ObjUtil; +import com.cool.core.request.R; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +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.setData( e.getData() ); + } else { + r.setCode( e.getCode() ); + r.setMessage( e.getMessage() ); + } + if (ObjUtil.isNotEmpty(e.getCause())) { + log.error(e.getCause().getMessage(), e.getCause()); + } + 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(); + } + + @ExceptionHandler(WxErrorException.class) + public R handleException(WxErrorException e) { + log.error(e.getMessage(), e); + return R.error(e.getMessage()); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/exception/CoolPreconditions.java b/cool-admin-java/src/main/java/com/cool/core/exception/CoolPreconditions.java new file mode 100644 index 0000000..64cbb0e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/exception/CoolPreconditions.java @@ -0,0 +1,110 @@ +package com.cool.core.exception; + +import cn.hutool.core.util.ObjectUtil; +import com.cool.core.request.R; +import com.cool.core.util.I18nUtil; +import java.util.Arrays; +import java.util.Optional; +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 getCoolException(message, code, arguments); + } + } + + public static void check(boolean flag, String message, Object... arguments) { + if (flag) { + throw getCoolException(message, arguments); + } + } + + public static void alwaysThrow(String message, Object... arguments) { + throw getCoolException(message, arguments); + } + + private static CoolException getCoolException(String message, Object... arguments) { + Optional first = Arrays.stream(arguments).filter(o -> o instanceof Throwable) + .findFirst(); + return new CoolException(formatMessage(message, arguments), (Throwable) first.orElse(null)); + } + + private static CoolException getCoolException(String message, int code, Object... arguments) { + Optional first = Arrays.stream(arguments).filter(o -> o instanceof Throwable) + .findFirst(); + return new CoolException(formatMessage(message, arguments), code, (Throwable) first.orElse(null)); + } + + + /** + * 返回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 returnNoData(Object object) { + if (ObjectUtil.isEmpty(object)) { + R r = R.ok(); + throw new CoolException(r.getMessage(), r.getCode()); + } + } + + /** + * 对象如果为空 就抛异常 + */ + public static void checkEmpty(Object object, String message, Object... arguments) { + check(ObjectUtil.isEmpty(object), formatMessage(message, arguments)); + } + + public static void checkEmpty(Object object) { + check(ObjectUtil.isEmpty(object), "参数不能为空"); + } + + private static String formatMessage(String messagePattern, Object... arguments) { + messagePattern = I18nUtil.getI18nMsg(messagePattern); + 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/cool-admin-java/src/main/java/com/cool/core/file/FileUploadStrategyFactory.java b/cool-admin-java/src/main/java/com/cool/core/file/FileUploadStrategyFactory.java new file mode 100644 index 0000000..ccbc7f4 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/file/FileUploadStrategyFactory.java @@ -0,0 +1,53 @@ +package com.cool.core.file; + +import static com.cool.core.plugin.consts.PluginConsts.uploadHook; + +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; + + private FileUploadStrategy getStrategy(PluginInfoEntity pluginInfoEntity) { + if (ObjUtil.isEmpty(pluginInfoEntity)) { + return applicationContext.getBean("localFileUploadStrategy", FileUploadStrategy.class); + } + return applicationContext.getBean("cloudFileUploadStrategy", 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() { + PluginInfoEntity pluginInfoEntity = coolPluginService.getPluginInfoEntityByHook(uploadHook); + String key = null; + if (ObjUtil.isNotEmpty(pluginInfoEntity)) { + key = pluginInfoEntity.getKey(); + } + return getStrategy(pluginInfoEntity).getMode(key); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/file/UpLoadModeType.java b/cool-admin-java/src/main/java/com/cool/core/file/UpLoadModeType.java new file mode 100644 index 0000000..eb99140 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/file/strategy/CloudFileUploadStrategy.java b/cool-admin-java/src/main/java/com/cool/core/file/strategy/CloudFileUploadStrategy.java new file mode 100644 index 0000000..4e449a0 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/file/strategy/CloudFileUploadStrategy.java @@ -0,0 +1,33 @@ +package com.cool.core.file.strategy; + +import com.cool.core.config.FileModeEnum; +import com.cool.core.util.CoolPluginInvokers; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component("cloudFileUploadStrategy") +public class CloudFileUploadStrategy implements FileUploadStrategy { + + @Override + public Object upload(MultipartFile[] files, HttpServletRequest request, PluginInfoEntity pluginInfoEntity) + throws IOException { + return CoolPluginInvokers.invokePlugin(pluginInfoEntity.getKey()); + } + + @Override + public Map getMode(String key) { + try{ + Object mode = CoolPluginInvokers.invoke(key, "getMode"); + if (Objects.nonNull(mode)) { + return (Map) mode; + } + } catch (Exception ignore){} + return Map.of("mode", FileModeEnum.CLOUD.value(), + "type", FileModeEnum.CLOUD.type()); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/file/strategy/FileUploadStrategy.java b/cool-admin-java/src/main/java/com/cool/core/file/strategy/FileUploadStrategy.java new file mode 100644 index 0000000..47bd605 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/file/strategy/FileUploadStrategy.java @@ -0,0 +1,38 @@ +package com.cool.core.file.strategy; + +import com.cool.modules.plugin.entity.PluginInfoEntity; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import org.springframework.web.multipart.MultipartFile; + +public interface FileUploadStrategy { + + /** + * 文件上传 + */ + Object upload(MultipartFile[] files, HttpServletRequest request, PluginInfoEntity pluginInfoEntity) + throws IOException; + + /** + * 文件上传模式 + * + * @return 上传模式 + */ + Map getMode(String key); + + 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/cool-admin-java/src/main/java/com/cool/core/file/strategy/LocalFileUploadStrategy.java b/cool-admin-java/src/main/java/com/cool/core/file/strategy/LocalFileUploadStrategy.java new file mode 100644 index 0000000..0be9d8e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/file/strategy/LocalFileUploadStrategy.java @@ -0,0 +1,74 @@ +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.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.Map; +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 date = DateUtil.format(new Date(), + DatePattern.PURE_DATE_PATTERN); + String absoluteUploadFolder = localFileProperties.getAbsoluteUploadFolder(); + String fullPath = absoluteUploadFolder + "/" + date; + FileUtil.mkdir(fullPath); + for (MultipartFile file : files) { + // 保存文件 + String fileName = StrUtil.uuid().replaceAll("-", "") + getExtensionName( + Objects.requireNonNull(file.getOriginalFilename())); + file.transferTo(new File(fullPath + + "/" + 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 Map getMode(String key) { + return Map.of("mode", FileModeEnum.LOCAL.value(), + "type", FileModeEnum.LOCAL.type()); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/i18n/I18nGenerator.java b/cool-admin-java/src/main/java/com/cool/core/i18n/I18nGenerator.java new file mode 100644 index 0000000..40741a5 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/i18n/I18nGenerator.java @@ -0,0 +1,219 @@ +package com.cool.core.i18n; + +import static com.cool.core.util.I18nUtil.*; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.core.lock.CoolLock; +import com.cool.core.util.CoolPluginInvokers; +import com.cool.core.util.I18nUtil; +import com.cool.core.util.PathUtils; +import com.cool.modules.base.entity.sys.BaseSysMenuEntity; +import com.cool.modules.base.service.sys.BaseSysMenuService; +import com.cool.modules.dict.entity.DictInfoEntity; +import com.cool.modules.dict.entity.DictTypeEntity; +import com.cool.modules.dict.service.DictInfoService; +import com.cool.modules.dict.service.DictTypeService; +import com.mybatisflex.core.query.QueryWrapper; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class I18nGenerator { + private final BaseSysMenuService baseSysMenuService; + private final DictTypeService dictTypeService; + private final DictInfoService dictInfoService; + private final CoolLock coolLock; + private final I18nUtil i18nUtil; + + private List languages; + private static final Duration DURATION = Duration.ofSeconds(30); + public void run(Map map) { + log.info("国际化 翻译..."); + languages = (List) map.getOrDefault("languages", List.of("zh-cn", "zh-tw", "en")); + path = (String) map.getOrDefault("path", "assets/i18n"); + init(); + log.info("✅国际化 翻译 成功!!!"); + enable = true; + } + + public void init() { + // 四个任务并发执行 + CompletableFuture futureMsg = CompletableFuture.runAsync(this::genBaseMsg); + CompletableFuture futureMenu = CompletableFuture.runAsync(this::genBaseMenu); + CompletableFuture futureDictInfo = CompletableFuture.runAsync(this::genBaseDictInfo); + CompletableFuture futureDictType = CompletableFuture.runAsync(this::genBaseDictType); + + // 等待全部执行完成 + CompletableFuture.allOf(futureMsg, futureMenu, futureDictInfo, futureDictType).join(); + } + + private void genBaseMsg() { + try { + Map msgMap = new HashMap<>(); + // 从idea本地启动时,从项目目录中读取 + Files.walk(Paths.get(System.getProperty("user.dir"))) + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".java")) + .filter(path -> !path.toString().contains("/target/") && !path.toString().contains("/.git/")) + .forEach(path -> msgMap.putAll(processFile(path))); + if (ObjUtil.isNotEmpty(msgMap)) { + // 系统异常信息,输出到resources/i18n 文件夹下,只有本地运行会生成 + File msgfile = FileUtil.file(PathUtils.getUserDir(), + "src", "main", "resources", "cool", "i18n", "msg", "template.json"); + // 确保父目录存在 + FileUtil.mkParentDirs(msgfile); + // 写入内容 + FileUtil.writeUtf8String(JSONUtil.toJsonStr(msgMap), msgfile); + } else { + try { + // jar启动时,从jar包中读取 + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource resource = resolver.getResource("classpath:cool/i18n/msg/template.json"); + String content = FileUtil.readUtf8String(resource.getFile()); + msgMap.putAll(JSONUtil.toBean(content, Map.class)); + } catch (Exception e) { + log.error("获取系统异常信息失败", e); + } + } + extracted(MSG_PREFIX, msgMap); + } catch (Exception e) { + log.error("国际化系统异常信息失败", e); + } + } + + /** + * 生成菜单信息国际化 + */ + @Async + public void asyncGenBaseMenu() { + if (coolLock.tryLock(MENU_PREFIX, DURATION)) { + genBaseMenu(); + coolLock.unlock(MENU_PREFIX); + } + } + private void genBaseMenu() { + try { + Map menuMap = baseSysMenuService.list(QueryWrapper.create().select(BaseSysMenuEntity::getName)) + .stream() + .collect(Collectors.toMap( + BaseSysMenuEntity::getName, + BaseSysMenuEntity::getName, + (oldValue, newValue) -> oldValue + )); + extracted(MENU_PREFIX, menuMap); + } catch (Exception e) { + log.error("国际化菜单信息失败", e); + } + } + @Async + public void asyncGenBaseDictType() { + if (coolLock.tryLock(DICT_TYPE_PREFIX, DURATION)) { + genBaseDictType(); + coolLock.unlock(DICT_TYPE_PREFIX); + } + } + private void genBaseDictType() { + try { + Map dataMap = dictTypeService.list(QueryWrapper.create().select(DictTypeEntity::getName)) + .stream() + .collect(Collectors.toMap( + DictTypeEntity::getName, + DictTypeEntity::getName, + (oldValue, newValue) -> oldValue + )); + extracted(DICT_TYPE_PREFIX, dataMap); + } catch (Exception e) { + log.error("国际化字段类型信息失败", e); + } + } + @Async + public void asyncGenBaseDictInfo() { + if (coolLock.tryLock(DICT_INFO_PREFIX, DURATION)) { + genBaseDictInfo(); + coolLock.unlock(DICT_INFO_PREFIX); + } + } + private void genBaseDictInfo() { + try { + Map dataMap = dictInfoService.list(QueryWrapper.create().select(DictInfoEntity::getName)) + .stream() + .collect(Collectors.toMap( + DictInfoEntity::getName, + DictInfoEntity::getName, + (oldValue, newValue) -> oldValue + )); + extracted(DICT_INFO_PREFIX, dataMap); + } catch (Exception e) { + log.error("国际化字段类型信息失败", e); + } + } + + private void extracted(String prefix, Map dataMap) { + languages.forEach(language -> { + String key = prefix + language; + if (!i18nUtil.exist(key)) { + JSONObject jsonObject = invokeTranslate(dataMap, language); + if (ObjUtil.isNotNull(jsonObject)) { + i18nUtil.update(key, jsonObject); + } + } + }); + } + + private JSONObject invokeTranslate(Map map, String language) { + return (JSONObject) CoolPluginInvokers.invoke("i18n", "invokeTranslate", map, language); + } + + // 匹配 CoolPreconditions 抛异常语句中的中文字符串 + private static final Pattern EXCEPTION_PATTERN = Pattern.compile( + "CoolPreconditions\\.(\\w+)\\s*\\([^;]*?\"([^\"]*[\u4e00-\u9fa5]+[^\"]*)\"", Pattern.MULTILINE + ); + + private static Map processFile(Path path) { + Map map = new HashMap<>(); + try { + String content = Files.readString(path, StandardCharsets.UTF_8); + // 去掉注释 + content = removeComments(content); + + // 仅查找方法体内的 CoolPreconditions 调用 + Matcher matcher = EXCEPTION_PATTERN.matcher(content); + while (matcher.find()) { + String chineseText = matcher.group(2).trim(); + map.put(chineseText, chineseText); + } + } catch (IOException e) { + e.printStackTrace(); + } + return map; + } + + // 移除注释(单行与多行) + private static String removeComments(String code) { + String noMultiLine = code.replaceAll("/\\*.*?\\*/", ""); // 多行注释 + return noMultiLine.replaceAll("//.*", ""); // 单行注释 + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/init/CoolPluginInit.java b/cool-admin-java/src/main/java/com/cool/core/init/CoolPluginInit.java new file mode 100644 index 0000000..846be54 --- /dev/null +++ b/cool-admin-java/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.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 历史安装过的插件执行初始化 + **/ +@Slf4j +@Component +@RequiredArgsConstructor +public class CoolPluginInit { + + final private CoolPluginService coolPluginService; + + @EventListener(ApplicationReadyEvent.class) + public void run() { + coolPluginService.init(); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/init/DBFromJsonInit.java b/cool-admin-java/src/main/java/com/cool/core/init/DBFromJsonInit.java new file mode 100644 index 0000000..9f01048 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/init/DBFromJsonInit.java @@ -0,0 +1,252 @@ +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.mybatis.pg.PostgresSequenceSyncService; +import com.cool.core.util.DatabaseDialectUtils; +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 com.mybatisflex.core.query.QueryWrapper; +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.Map; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Component; + +/** + * 数据库初始数据初始化 在 classpath:cool/data/db 目录下创建.json文件 并定义表数据, 由该类统一执行初始化 + **/ +@Slf4j +@Component +@RequiredArgsConstructor +public class DBFromJsonInit { + + final private BaseSysConfService baseSysConfService; + + final private BaseSysMenuService baseSysMenuService; + + final private MapperProviderService mapperProviderService; + + final private ApplicationEventPublisher eventPublisher; + + final private PostgresSequenceSyncService postgresSequenceSyncService; + + @Value("${cool.initData}") + private boolean initData; + + @EventListener(ApplicationReadyEvent.class) + public void run() { + if (!initData) { + return; + } + // 初始化自定义的数据 + boolean initFlag = extractedDb(); + // 初始化菜单数据 + initFlag = extractedMenu() || initFlag; + // 发送数据库初始化完成事件 + eventPublisher.publishEvent(new DbInitCompleteEvent(this)); + if (initFlag) { + // 如果是postgresql,同步序列 + syncIdentitySequences(); + } + log.info("数据初始化完成!"); + } + + private void syncIdentitySequences() { + if (DatabaseDialectUtils.isPostgresql()) { + postgresSequenceSyncService.syncIdentitySequences(); + } + } + + @Getter + public static class DbInitCompleteEvent { + private final Object source; + + public DbInitCompleteEvent(Object source) { + this.source = source; + } + + } + + /** + * 解析插入业务数据 + */ + private boolean extractedDb() { + try { + // 加载 JSON 文件 + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources("classpath:cool/data/db/*.json"); + // 遍历所有.json文件 + return analysisResources(resources); + } catch (Exception e) { + log.error("Failed to initialize data", e); + } + return false; + } + + private boolean analysisResources(Resource[] resources) + throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + String prefix = "db_"; + boolean isInit = false; + 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); + isInit = true; + log.info("{} 业务数据初始化成功...", fileName); + } + return isInit; + } + + 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 { + // 插入数据 + 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; + } + if (ObjUtil.isNotEmpty(id)) { + // 带id插入 + baseMapper.insertSelectiveWithPk(entity); + } else { + baseMapper.insert(entity); + } + } + } + + /** + * 解析插入菜单数据 + */ + public boolean extractedMenu() { + boolean initFlag = false; + 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); + initFlag = true; + } + } catch (Exception e) { + log.error("Failed to initialize data", e); + } + return initFlag; + } + + 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()); + } + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(BaseSysMenuEntity::getName, menuEntity.getName()); + if (ObjUtil.isNull(menuEntity.getParentId())) { + queryWrapper.isNull(BaseSysMenuEntity::getParentId); + } else { + queryWrapper.eq(BaseSysMenuEntity::getParentId, menuEntity.getParentId()); + } + BaseSysMenuEntity dbBaseSysMenuEntity = baseSysMenuService.getOne(queryWrapper); + if (ObjUtil.isNull(dbBaseSysMenuEntity)) { + baseSysMenuService.add(menuEntity); + } else { + menuEntity = dbBaseSysMenuEntity; + } + // 递归处理子菜单 + JSONArray childMenus = jsonObj.getJSONArray("childMenus"); + if (childMenus != null) { + for (Object obj : childMenus) { + JSONObject childObj = (JSONObject) obj; + parseMenu(childObj, menuEntity); + } + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/init/IDGenInit.java b/cool-admin-java/src/main/java/com/cool/core/init/IDGenInit.java new file mode 100644 index 0000000..446e59a --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/init/IDGenInit.java @@ -0,0 +1,24 @@ +package com.cool.core.init; + +import com.cool.core.leaf.IDGenService; +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; + +/** + * 唯一ID 组件初始化 + **/ +@Slf4j +@Component +@RequiredArgsConstructor +public class IDGenInit { + + final private IDGenService idGenService; + + @EventListener(ApplicationReadyEvent.class) + public void run() { + idGenService.init(); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/IDGenService.java b/cool-admin-java/src/main/java/com/cool/core/leaf/IDGenService.java new file mode 100644 index 0000000..b004ce6 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/IDGenService.java @@ -0,0 +1,6 @@ +package com.cool.core.leaf; + +public interface IDGenService { + long next(String key); + void init(); +} diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/common/CheckVO.java b/cool-admin-java/src/main/java/com/cool/core/leaf/common/CheckVO.java new file mode 100644 index 0000000..0d207a7 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/common/CheckVO.java @@ -0,0 +1,27 @@ +package com.cool.core.leaf.common; + +public class CheckVO { + private long timestamp; + private int workID; + + public CheckVO(long timestamp, int workID) { + this.timestamp = timestamp; + this.workID = workID; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public int getWorkID() { + return workID; + } + + public void setWorkID(int workID) { + this.workID = workID; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/common/Result.java b/cool-admin-java/src/main/java/com/cool/core/leaf/common/Result.java new file mode 100644 index 0000000..d85b1c6 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/common/Result.java @@ -0,0 +1,39 @@ +package com.cool.core.leaf.common; + +public class Result { + private long id; + private Status status; + + public Result() { + + } + public Result(long id, Status status) { + this.id = id; + this.status = status; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Result{"); + sb.append("id=").append(id); + sb.append(", status=").append(status); + sb.append('}'); + return sb.toString(); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/common/Status.java b/cool-admin-java/src/main/java/com/cool/core/leaf/common/Status.java new file mode 100644 index 0000000..ec615e9 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/common/Status.java @@ -0,0 +1,6 @@ +package com.cool.core.leaf.common; + +public enum Status { + SUCCESS, + EXCEPTION +} diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/package-info.java b/cool-admin-java/src/main/java/com/cool/core/leaf/package-info.java new file mode 100644 index 0000000..8278bae --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/package-info.java @@ -0,0 +1,5 @@ +/** + * 全局唯一id生成 + * 来源美团:https://github.com/Meituan-Dianping/Leaf + */ +package com.cool.core.leaf; diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/segment/SegmentIDGenImpl.java b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/SegmentIDGenImpl.java new file mode 100644 index 0000000..0c5825b --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/SegmentIDGenImpl.java @@ -0,0 +1,310 @@ +package com.cool.core.leaf.segment; + +import static com.cool.core.leaf.segment.entity.table.LeafAllocEntityTableDef.LEAF_ALLOC_ENTITY; + +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.leaf.IDGenService; +import com.cool.core.leaf.common.Result; +import com.cool.core.leaf.common.Status; +import com.cool.core.leaf.segment.entity.LeafAllocEntity; +import com.cool.core.leaf.segment.mapper.LeafAllocMapper; +import com.cool.core.leaf.segment.model.Segment; +import com.cool.core.leaf.segment.model.SegmentBuffer; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.update.UpdateChain; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; +import lombok.RequiredArgsConstructor; +import org.perf4j.StopWatch; +import org.perf4j.slf4j.Slf4JStopWatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SegmentIDGenImpl implements IDGenService, DisposableBean { + + private static final Logger logger = LoggerFactory.getLogger(SegmentIDGenImpl.class); + + @Value("${leaf.segment.enable:false}") + private boolean enable; + + /** + * IDCache未初始化成功时的异常码 + */ + private static final long EXCEPTION_ID_IDCACHE_INIT_FALSE = -1; + /** + * key不存在时的异常码 + */ + private static final long EXCEPTION_ID_KEY_NOT_EXISTS = -2; + /** + * SegmentBuffer中的两个Segment均未从DB中装载时的异常码 + */ + private static final long EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL = -3; + /** + * 最大步长不超过100,0000 + */ + private static final int MAX_STEP = 1000000; + /** + * 一个Segment维持时间为15分钟 + */ + private static final long SEGMENT_DURATION = 15 * 60 * 1000L; + private final ExecutorService executorService = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), new UpdateThreadFactory()); + private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r); + t.setName("check-idCache-thread"); + t.setDaemon(true); + return t; + }); + private volatile boolean initOK = false; + private final Map cache = new ConcurrentHashMap<>(); + private final LeafAllocMapper leafAllocMapper; + + public static class UpdateThreadFactory implements ThreadFactory { + + private static int threadInitNumber = 0; + + private static synchronized int nextThreadNum() { + return threadInitNumber++; + } + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "Thread-Segment-Update-" + nextThreadNum()); + } + } + + @Override + public long next(String key) { + Result result = get(key); + CoolPreconditions.check(result.getId() < 0, "获取失败,code值: {}", result.getId()); + return result.getId(); + } + + @Override + public void init() { + if (enable) { + // 确保加载到kv后才初始化成功 + updateCacheFromDb(); + initOK = true; + updateCacheFromDbAtEveryMinute(); + logger.info("唯一ID组件初始化成功 ..."); + } + } + + private void updateCacheFromDbAtEveryMinute() { + scheduledExecutorService.scheduleWithFixedDelay(this::updateCacheFromDb, 60, 60, TimeUnit.SECONDS); + } + + private void updateCacheFromDb() { + logger.info("update cache from db"); + StopWatch sw = new Slf4JStopWatch(); + try { + List dbTags = leafAllocMapper.selectListByQuery(QueryWrapper.create().select( + LeafAllocEntity::getKey)).stream().map(LeafAllocEntity::getKey).toList(); + if (dbTags.isEmpty()) { + return; + } + List cacheTags = new ArrayList(cache.keySet()); + Set insertTagsSet = new HashSet<>(dbTags); + Set removeTagsSet = new HashSet<>(cacheTags); + //db中新加的tags灌进cache + for (String tmp : cacheTags) { + insertTagsSet.remove(tmp); + } + for (String tag : insertTagsSet) { + SegmentBuffer buffer = new SegmentBuffer(); + buffer.setKey(tag); + Segment segment = buffer.getCurrent(); + segment.setValue(new AtomicLong(0)); + segment.setMax(0); + segment.setStep(0); + cache.put(tag, buffer); + logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer); + } + //cache中已失效的tags从cache删除 + for (String tmp : dbTags) { + removeTagsSet.remove(tmp); + } + for (String tag : removeTagsSet) { + cache.remove(tag); + logger.info("Remove tag {} from IdCache", tag); + } + } catch (Exception e) { + logger.warn("update cache from db exception", e); + } finally { + sw.stop("updateCacheFromDb"); + } + } + + private Result get(final String key) { + if (!initOK) { + return new Result(EXCEPTION_ID_IDCACHE_INIT_FALSE, Status.EXCEPTION); + } + CoolPreconditions.check(!initOK, "IDCache未初始化成功"); + if (cache.containsKey(key)) { + SegmentBuffer buffer = cache.get(key); + if (!buffer.isInitOk()) { + synchronized (buffer) { + if (!buffer.isInitOk()) { + try { + updateSegmentFromDb(key, buffer.getCurrent()); + logger.info("Init buffer. Update leafkey {} {} from db", key, buffer.getCurrent()); + buffer.setInitOk(true); + } catch (Exception e) { + logger.warn("Init buffer {} exception", buffer.getCurrent(), e); + } + } + } + } + return getIdFromSegmentBuffer(cache.get(key)); + } + return new Result(EXCEPTION_ID_KEY_NOT_EXISTS, Status.EXCEPTION); + } + + public void updateSegmentFromDb(String key, Segment segment) { + StopWatch sw = new Slf4JStopWatch(); + SegmentBuffer buffer = segment.getBuffer(); + LeafAllocEntity leafAllocEntity; + if (!buffer.isInitOk()) { + leafAllocEntity = updateMaxIdAndGetLeafAlloc(key); + buffer.setStep(leafAllocEntity.getStep()); + buffer.setMinStep(leafAllocEntity.getStep());//leafAlloc中的step为DB中的step + } else if (buffer.getUpdateTimestamp() == 0) { + leafAllocEntity = updateMaxIdAndGetLeafAlloc(key); + buffer.setUpdateTimestamp(System.currentTimeMillis()); + buffer.setStep(leafAllocEntity.getStep()); + buffer.setMinStep(leafAllocEntity.getStep());//leafAlloc中的step为DB中的step + } else { + long duration = System.currentTimeMillis() - buffer.getUpdateTimestamp(); + int nextStep = buffer.getStep(); + if (duration < SEGMENT_DURATION) { + if (nextStep * 2 > MAX_STEP) { + //do nothing + } else { + nextStep = nextStep * 2; + } + } else if (duration < SEGMENT_DURATION * 2) { + //do nothing with nextStep + } else { + nextStep = nextStep / 2 >= buffer.getMinStep() ? nextStep / 2 : nextStep; + } + logger.info("leafKey[{}], step[{}], duration[{}mins], nextStep[{}]", key, buffer.getStep(), String.format("%.2f",((double)duration / (1000 * 60))), nextStep); + LeafAllocEntity temp = new LeafAllocEntity(); + temp.setKey(key); + temp.setStep(nextStep); + leafAllocEntity = updateMaxIdByCustomStepAndGetLeafAlloc(temp); + buffer.setUpdateTimestamp(System.currentTimeMillis()); + buffer.setStep(nextStep); + buffer.setMinStep(leafAllocEntity.getStep());//leafAlloc的step为DB中的step + } + // must set value before set max + long value = leafAllocEntity.getMaxId() - buffer.getStep(); + segment.getValue().set(value); + segment.setMax(leafAllocEntity.getMaxId()); + segment.setStep(buffer.getStep()); + sw.stop("updateSegmentFromDb", key + " " + segment); + } + + private LeafAllocEntity updateMaxIdByCustomStepAndGetLeafAlloc(LeafAllocEntity temp) { + UpdateChain.of(LeafAllocEntity.class) + .setRaw(LeafAllocEntity::getMaxId, LEAF_ALLOC_ENTITY.MAX_ID.getName() + " + " + temp.getStep()) + .where(LeafAllocEntity::getKey).eq(temp.getKey()) + .update(); + return leafAllocMapper.selectOneByQuery(QueryWrapper.create().select( + LEAF_ALLOC_ENTITY.KEY, LEAF_ALLOC_ENTITY.MAX_ID, LEAF_ALLOC_ENTITY.STEP).eq(LeafAllocEntity::getKey, temp.getKey())); + } + + private LeafAllocEntity updateMaxIdAndGetLeafAlloc(String key) { + UpdateChain.of(LeafAllocEntity.class) + .setRaw(LeafAllocEntity::getMaxId, LEAF_ALLOC_ENTITY.MAX_ID.getName() + " + " + LEAF_ALLOC_ENTITY.STEP.getName()) + .where(LeafAllocEntity::getKey).eq(key) + .update(); + return leafAllocMapper.selectOneByQuery(QueryWrapper.create().select( + LEAF_ALLOC_ENTITY.KEY, LEAF_ALLOC_ENTITY.MAX_ID, LEAF_ALLOC_ENTITY.STEP).eq(LeafAllocEntity::getKey, key)); + } + + public Result getIdFromSegmentBuffer(final SegmentBuffer buffer) { + while (true) { + buffer.rLock().lock(); + try { + final Segment segment = buffer.getCurrent(); + if (!buffer.isNextReady() && (segment.getIdle() < 0.9 * segment.getStep()) && buffer.getThreadRunning().compareAndSet(false, true)) { + executorService.execute(() -> { + Segment next = buffer.getSegments()[buffer.nextPos()]; + boolean updateOk = false; + try { + updateSegmentFromDb(buffer.getKey(), next); + updateOk = true; + logger.info("update segment {} from db {}", buffer.getKey(), next); + } catch (Exception e) { + logger.warn(buffer.getKey() + " updateSegmentFromDb exception", e); + } finally { + if (updateOk) { + buffer.wLock().lock(); + buffer.setNextReady(true); + buffer.getThreadRunning().set(false); + buffer.wLock().unlock(); + } else { + buffer.getThreadRunning().set(false); + } + } + }); + } + long value = segment.getValue().getAndIncrement(); + if (value < segment.getMax()) { + return new Result(value, Status.SUCCESS); + } + } finally { + buffer.rLock().unlock(); + } + waitAndSleep(buffer); + buffer.wLock().lock(); + try { + final Segment segment = buffer.getCurrent(); + long value = segment.getValue().getAndIncrement(); + if (value < segment.getMax()) { + return new Result(value, Status.SUCCESS); + } + if (buffer.isNextReady()) { + buffer.switchPos(); + buffer.setNextReady(false); + } else { + logger.error("Both two segments in {} are not ready!", buffer); + return new Result(EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL, Status.EXCEPTION); + } + } finally { + buffer.wLock().unlock(); + } + } + } + + private void waitAndSleep(SegmentBuffer buffer) { + int roll = 0; + while (buffer.getThreadRunning().get()) { + roll += 1; + if(roll > 10000) { + try { + TimeUnit.MILLISECONDS.sleep(10); + break; + } catch (InterruptedException e) { + logger.warn("Thread {} Interrupted",Thread.currentThread().getName()); + break; + } + } + } + } + + @Override + public void destroy() throws Exception { + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + + scheduledExecutorService.shutdown(); + scheduledExecutorService.awaitTermination(10, TimeUnit.SECONDS); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/segment/entity/LeafAllocEntity.java b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/entity/LeafAllocEntity.java new file mode 100644 index 0000000..59a3f85 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/entity/LeafAllocEntity.java @@ -0,0 +1,27 @@ +package com.cool.core.leaf.segment.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import com.tangzc.mybatisflex.autotable.annotation.UniIndex; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "leaf_alloc", comment = "唯一id分配") +public class LeafAllocEntity extends BaseEntity { + + @UniIndex(name = "uk_key") + @ColumnDefine(comment = "业务key ,比如orderId", length = 20, notNull = true) + private String key; + + @ColumnDefine(comment = "当前最大id", defaultValue = "1", notNull = true) + private Long maxId; + + @ColumnDefine(comment = "步长", defaultValue = "500", notNull = true) + private Integer step; + + @ColumnDefine(comment = "描述") + private String description; +} diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/segment/mapper/LeafAllocMapper.java b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/mapper/LeafAllocMapper.java new file mode 100644 index 0000000..1f492b6 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/mapper/LeafAllocMapper.java @@ -0,0 +1,7 @@ +package com.cool.core.leaf.segment.mapper; + +import com.cool.core.leaf.segment.entity.LeafAllocEntity; +import com.mybatisflex.core.BaseMapper; + +public interface LeafAllocMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/segment/model/Segment.java b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/model/Segment.java new file mode 100644 index 0000000..6908a36 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/model/Segment.java @@ -0,0 +1,59 @@ +package com.cool.core.leaf.segment.model; + +import java.util.concurrent.atomic.AtomicLong; + +public class Segment { + private AtomicLong value = new AtomicLong(0); + private volatile long max; + private volatile int step; + private SegmentBuffer buffer; + + public Segment(SegmentBuffer buffer) { + this.buffer = buffer; + } + + public AtomicLong getValue() { + return value; + } + + public void setValue(AtomicLong value) { + this.value = value; + } + + public long getMax() { + return max; + } + + public void setMax(long max) { + this.max = max; + } + + public int getStep() { + return step; + } + + public void setStep(int step) { + this.step = step; + } + + public SegmentBuffer getBuffer() { + return buffer; + } + + public long getIdle() { + return this.getMax() - getValue().get(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Segment("); + sb.append("value:"); + sb.append(value); + sb.append(",max:"); + sb.append(max); + sb.append(",step:"); + sb.append(step); + sb.append(")"); + return sb.toString(); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/leaf/segment/model/SegmentBuffer.java b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/model/SegmentBuffer.java new file mode 100644 index 0000000..44c33da --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/leaf/segment/model/SegmentBuffer.java @@ -0,0 +1,129 @@ +package com.cool.core.leaf.segment.model; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * 双buffer + */ +public class SegmentBuffer { + private String key; + private Segment[] segments; //双buffer + private volatile int currentPos; //当前的使用的segment的index + private volatile boolean nextReady; //下一个segment是否处于可切换状态 + private volatile boolean initOk; //是否初始化完成 + private final AtomicBoolean threadRunning; //线程是否在运行中 + private final ReadWriteLock lock; + + private volatile int step; + private volatile int minStep; + private volatile long updateTimestamp; + + public SegmentBuffer() { + segments = new Segment[]{new Segment(this), new Segment(this)}; + currentPos = 0; + nextReady = false; + initOk = false; + threadRunning = new AtomicBoolean(false); + lock = new ReentrantReadWriteLock(); + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Segment[] getSegments() { + return segments; + } + + public Segment getCurrent() { + return segments[currentPos]; + } + + public int getCurrentPos() { + return currentPos; + } + + public int nextPos() { + return (currentPos + 1) % 2; + } + + public void switchPos() { + currentPos = nextPos(); + } + + public boolean isInitOk() { + return initOk; + } + + public void setInitOk(boolean initOk) { + this.initOk = initOk; + } + + public boolean isNextReady() { + return nextReady; + } + + public void setNextReady(boolean nextReady) { + this.nextReady = nextReady; + } + + public AtomicBoolean getThreadRunning() { + return threadRunning; + } + + public Lock rLock() { + return lock.readLock(); + } + + public Lock wLock() { + return lock.writeLock(); + } + + public int getStep() { + return step; + } + + public void setStep(int step) { + this.step = step; + } + + public int getMinStep() { + return minStep; + } + + public void setMinStep(int minStep) { + this.minStep = minStep; + } + + public long getUpdateTimestamp() { + return updateTimestamp; + } + + public void setUpdateTimestamp(long updateTimestamp) { + this.updateTimestamp = updateTimestamp; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("SegmentBuffer{"); + sb.append("key='").append(key).append('\''); + sb.append(", segments=").append(Arrays.toString(segments)); + sb.append(", currentPos=").append(currentPos); + sb.append(", nextReady=").append(nextReady); + sb.append(", initOk=").append(initOk); + sb.append(", threadRunning=").append(threadRunning); + sb.append(", step=").append(step); + sb.append(", minStep=").append(minStep); + sb.append(", updateTimestamp=").append(updateTimestamp); + sb.append('}'); + return sb.toString(); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/lock/CoolLock.java b/cool-admin-java/src/main/java/com/cool/core/lock/CoolLock.java new file mode 100644 index 0000000..8ff0a80 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/lock/CoolLock.java @@ -0,0 +1,108 @@ +package com.cool.core.lock; + + +import jakarta.annotation.PostConstruct; +import java.time.Duration; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.cache.CacheType; +import org.springframework.cache.CacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CoolLock { + // 缓存类型 + @Value("${spring.cache.type}") + private String type; + + @Value("${cool.cacheName}") + private String cacheName; + + private final CacheManager cacheManager; + private RedisCacheWriter redisCache ; + + // 非redis方式时使用 + private static final Map lockMap = new ConcurrentHashMap<>(); + + private static final String LOCK_PREFIX = "lock:"; + + @PostConstruct + private void init() { + this.type = type.toLowerCase(); + if (type.equalsIgnoreCase(CacheType.REDIS.name())) { + redisCache = (RedisCacheWriter) Objects.requireNonNull(cacheManager.getCache(cacheName)) + .getNativeCache(); + } + } + /** + * 尝试获取锁 + * + * @param key 锁的 key + * @param expireTime 锁的过期时间 + * @return 如果成功获取锁则返回 true,否则返回 false + */ + public boolean tryLock(String key, Duration expireTime) { + String lockKey = getLockKey(key); + if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) { + Lock lock = lockMap.computeIfAbsent(lockKey, k -> new ReentrantLock()); + return lock.tryLock(); + } + byte[] lockKeyBytes = lockKey.getBytes(); + // 使用 putIfAbsent 来尝试设置锁,如果成功返回 true,否则返回 false + return redisCache.putIfAbsent(cacheName, lockKeyBytes, new byte[0], expireTime) == null; + } + + /** + * 释放锁 + */ + public void unlock(String key) { + String lockKey = getLockKey(key); + if (type.equalsIgnoreCase(CacheType.CAFFEINE.name())) { + Lock lock = lockMap.get(lockKey); + if (lock != null && lock.tryLock()) { + lock.unlock(); + lockMap.remove(lockKey); + } + return; + } + redisCache.remove(cacheName, lockKey.getBytes()); + } + + /** + * 拼接锁前缀 + */ + private String getLockKey(String key) { + return LOCK_PREFIX + key; + } + + /** + * 等待锁 + * + * @param key 锁的 key + * @param expireTime 锁的过期时间 + * @return 如果成功获取锁则返回 true,否则返回 false + */ + public boolean waitForLock(String key, Duration expireTime, Duration waitTime) { + long endTime = System.currentTimeMillis() + waitTime.toMillis(); + while (System.currentTimeMillis() < endTime) { + if (tryLock(key, expireTime)) { + return true; + } + // 等待锁释放 + try { + Thread.sleep(100); // 可以根据需要调整等待时间 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/mybatis/handler/BaseJsonTypeHandler.java b/cool-admin-java/src/main/java/com/cool/core/mybatis/handler/BaseJsonTypeHandler.java new file mode 100644 index 0000000..a403c46 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/mybatis/handler/BaseJsonTypeHandler.java @@ -0,0 +1,48 @@ +package com.cool.core.mybatis.handler; + +import com.cool.core.util.DatabaseDialectUtils; +import com.mybatisflex.core.util.StringUtil; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.postgresql.util.PGobject; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +public abstract class BaseJsonTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { + if (DatabaseDialectUtils.isPostgresql()) { + PGobject jsonObject = new PGobject(); + jsonObject.setType("json"); + jsonObject.setValue(toJson(parameter)); + ps.setObject(i, jsonObject); + } else { + ps.setString(i, toJson(parameter)); + } + } + + @Override + public T getNullableResult(ResultSet rs, String columnName) throws SQLException { + final String json = rs.getString(columnName); + return StringUtil.noText(json) ? null : parseJson(json); + } + + @Override + public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + final String json = rs.getString(columnIndex); + return StringUtil.noText(json) ? null : parseJson(json); + } + + @Override + public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + final String json = cs.getString(columnIndex); + return StringUtil.noText(json) ? null : parseJson(json); + } + + protected abstract T parseJson(String json); + + protected abstract String toJson(T object); + +} diff --git a/cool-admin-java/src/main/java/com/cool/core/mybatis/handler/Fastjson2TypeHandler.java b/cool-admin-java/src/main/java/com/cool/core/mybatis/handler/Fastjson2TypeHandler.java new file mode 100644 index 0000000..aad475e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/mybatis/handler/Fastjson2TypeHandler.java @@ -0,0 +1,73 @@ +package com.cool.core.mybatis.handler; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.TypeReference; + +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; + +public class Fastjson2TypeHandler extends BaseJsonTypeHandler { + + private final Class propertyType; + private Class genericType; + private Type type; + + private boolean supportAutoType = false; + + public Fastjson2TypeHandler(Class propertyType) { + this.propertyType = propertyType; + this.supportAutoType = propertyType.isInterface() || Modifier.isAbstract(propertyType.getModifiers()); + } + + + public Fastjson2TypeHandler(Class propertyType, Class genericType) { + this.propertyType = propertyType; + this.genericType = genericType; + this.type = TypeReference.collectionType((Class) propertyType, genericType); + + Type actualTypeArgument = ((ParameterizedType) type).getActualTypeArguments()[0]; + if (actualTypeArgument instanceof Class) { + this.supportAutoType = ((Class) actualTypeArgument).isInterface() + || Modifier.isAbstract(((Class) actualTypeArgument).getModifiers()); + } + } + + @Override + protected Object parseJson(String json) { + if (genericType != null && Collection.class.isAssignableFrom(propertyType)) { + if (supportAutoType) { + return JSON.parseArray(json, Object.class, JSONReader.Feature.SupportAutoType); + } else { + return JSON.parseObject(json, type); + } + + } else { + if (supportAutoType) { + return JSON.parseObject(json, Object.class, JSONReader.Feature.SupportAutoType); + } else { + return JSON.parseObject(json, propertyType); + } + } + } + + @Override + protected String toJson(Object object) { + if (supportAutoType) { + return JSON.toJSONString(object + , JSONWriter.Feature.WriteMapNullValue + , JSONWriter.Feature.WriteNullListAsEmpty + , JSONWriter.Feature.WriteNullStringAsEmpty, JSONWriter.Feature.WriteClassName + ); + } else { + return JSON.toJSONString(object + , JSONWriter.Feature.WriteMapNullValue + , JSONWriter.Feature.WriteNullListAsEmpty + , JSONWriter.Feature.WriteNullStringAsEmpty + ); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/mybatis/handler/JacksonTypeHandler.java b/cool-admin-java/src/main/java/com/cool/core/mybatis/handler/JacksonTypeHandler.java new file mode 100644 index 0000000..e92d096 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/mybatis/handler/JacksonTypeHandler.java @@ -0,0 +1,68 @@ +package com.cool.core.mybatis.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mybatisflex.core.exception.FlexExceptions; + +import java.io.IOException; +import java.util.Collection; + +public class JacksonTypeHandler extends BaseJsonTypeHandler { + + private static ObjectMapper objectMapper; + private final Class propertyType; + private Class genericType; + private JavaType javaType; + + public JacksonTypeHandler(Class propertyType) { + this.propertyType = propertyType; + } + + public JacksonTypeHandler(Class propertyType, Class genericType) { + this.propertyType = propertyType; + this.genericType = genericType; + } + + @Override + protected Object parseJson(String json) { + try { + if (genericType != null && Collection.class.isAssignableFrom(propertyType)) { + return getObjectMapper().readValue(json, getJavaType()); + } else { + return getObjectMapper().readValue(json, propertyType); + } + } catch (IOException e) { + throw FlexExceptions.wrap(e, "Can not parseJson by JacksonTypeHandler: " + json); + } + } + + @Override + protected String toJson(Object object) { + try { + return getObjectMapper().writeValueAsString(object); + } catch (JsonProcessingException e) { + throw FlexExceptions.wrap(e, "Can not convert object to Json by JacksonTypeHandler: " + object); + } + } + + + public JavaType getJavaType() { + if (javaType == null){ + javaType = getObjectMapper().getTypeFactory().constructCollectionType((Class) propertyType, genericType); + } + return javaType; + } + + public static ObjectMapper getObjectMapper() { + if (null == objectMapper) { + objectMapper = new ObjectMapper(); + } + return objectMapper; + } + + public static void setObjectMapper(ObjectMapper objectMapper) { + JacksonTypeHandler.objectMapper = objectMapper; + } + +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/mybatis/pg/PostgresSequenceSyncService.java b/cool-admin-java/src/main/java/com/cool/core/mybatis/pg/PostgresSequenceSyncService.java new file mode 100644 index 0000000..d54b34c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/mybatis/pg/PostgresSequenceSyncService.java @@ -0,0 +1,66 @@ +package com.cool.core.mybatis.pg; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; +import java.util.Map; +/** + * PostgreSQL Identity 序列同步服务 + * 解决PostgreSQL 默认的序列机制,序列会自动递增,当手动插入指定id时需调用同步接口,否则id会重复。 + */ +@Slf4j +@Service +public class PostgresSequenceSyncService { + + private final JdbcTemplate jdbcTemplate; + + public PostgresSequenceSyncService(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void syncIdentitySequences() { + log.info("⏳ 开始同步 PostgreSQL Identity 序列..."); + + // 查询所有 identity 字段 + String identityColumnQuery = """ + SELECT table_schema, table_name, column_name + FROM information_schema.columns + WHERE is_identity = 'YES' + AND table_schema = 'public' + """; + + List> identityColumns = jdbcTemplate.queryForList(identityColumnQuery); + + for (Map col : identityColumns) { + String schema = (String) col.get("table_schema"); + String table = (String) col.get("table_name"); + String column = (String) col.get("column_name"); + + String fullTable = schema + "." + table; + + // 获取对应的序列名 + String seqNameSql = "SELECT pg_get_serial_sequence(?, ?)"; + String seqName = jdbcTemplate.queryForObject(seqNameSql, String.class, fullTable, column); + + if (seqName == null) { + log.warn("⚠️ 无法获取序列:{}.{}", table, column); + continue; + } + + // 获取当前最大 ID + Long maxId = jdbcTemplate.queryForObject( + String.format("SELECT COALESCE(MAX(%s), 0) FROM %s", column, fullTable), + Long.class + ); + + if (maxId != null && maxId > 0) { // 正确的:setval 有返回值,必须用 queryForObject + String setvalSql = "SELECT setval(?, ?)"; + Long newVal = jdbcTemplate.queryForObject(setvalSql, Long.class, seqName, maxId); + log.info("✅ 同步序列 [{}] -> 当前最大 ID: {}", seqName, newVal); + } + } + + log.info("✅ PostgreSQL Identity 序列同步完成。"); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/request/CrudOption.java b/cool-admin-java/src/main/java/com/cool/core/request/CrudOption.java new file mode 100644 index 0000000..f4d1a47 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/request/CrudOption.java @@ -0,0 +1,225 @@ +package com.cool.core.request; + + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import com.cool.core.enums.QueryModeEnum; +import com.cool.core.util.ConvertUtil; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.query.QueryColumn; +import com.mybatisflex.core.query.QueryCondition; +import com.mybatisflex.core.query.QueryTable; +import com.mybatisflex.core.query.QueryWrapper; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +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 QueryModeEnum queryModeEnum; + + private TransformValue transformValue; + + private TransformParam transformParam; + + public interface TransformValue { + void apply(B obj); + } + + public interface TransformParam { + void apply(JSONObject obj); + } + + /** + * queryModeEnum 为 CUSTOM,可设置 默认为Map + */ + private Class asType; + + private Environment evn; + + public CrudOption(JSONObject requestParams) { + this.requestParams = requestParams; + this.queryWrapper = QueryWrapper.create(); + this.evn = SpringUtil.getBean(Environment.class); + queryModeEnum = QueryModeEnum.ENTITY; + } + + public QueryWrapper getQueryWrapper(Class entityClass) { + return build(this.queryWrapper, entityClass); + } + + public CrudOption queryWrapper(QueryWrapper queryWrapper) { + this.queryWrapper = queryWrapper; + return this; + } + + /** + * 按前端传上来的字段值做eq + */ + public CrudOption fieldEq(QueryColumn... fields) { + this.fieldEq = fields; + return this; + } + + /** + * 按前端传上来的字段值做like + */ + public CrudOption keyWordLikeFields(QueryColumn... fields) { + this.keyWordLikeFields = fields; + return this; + } + + /** + * 需要返回给前端的字段 + */ + public CrudOption select(QueryColumn... selects) { + this.select = selects; + return this; + } + + /** + * 查询模式决定返回值 + * 目前有三种模式,按实体查询返回、关联查询返回(实体字段上加 @RelationOneToMany 等注解)、自定义返回结果 + */ + public CrudOption queryModeEnum(QueryModeEnum queryModeEnum) { + this.queryModeEnum = queryModeEnum; + if (ObjUtil.equal(queryModeEnum, QueryModeEnum.CUSTOM) + && ObjUtil.isEmpty(asType)) { + asType = Map.class; + } + return this; + } + + /** + * 自定义返回结果对象类型 + */ + public CrudOption asType(Class asType) { + this.asType = asType; + return this; + } + + /** + * 转换值,组装数据 + */ + public CrudOption transformValue(TransformValue transformValue) { + this.transformValue = transformValue; + return this; + } + + /** + * 请使用 transformValue + */ + @Deprecated + public CrudOption transform(TransformValue transformValue) { + this.transformValue = transformValue; + return this; + } + + /** + * 转换入参 + */ + public CrudOption transformParam(TransformParam transformParam) { + this.transformParam = transformParam; + return this; + } + + /** + * 构建查询条件 + * + * @return QueryWrapper + */ + private QueryWrapper build(QueryWrapper queryWrapper, Class entityClass) { + if (ObjectUtil.isNotEmpty(fieldEq)) { + Arrays.stream(fieldEq).toList().forEach(filed -> { + String filedName = StrUtil.toCamelCase(filed.getName()); + Object obj = requestParams.get(filedName); + if (ObjUtil.isEmpty(obj)) { + return; + } + if (obj instanceof JSONArray) { + // 集合 + queryWrapper.and(filed.in(ConvertUtil.covertListByClass(filedName, (JSONArray)obj, entityClass).toArray())); + } else { + // 对象 + queryWrapper.and(filed.eq(ConvertUtil.convertByClass(filedName, obj, entityClass))); + } + }); + } + if (ObjectUtil.isNotEmpty(this.keyWordLikeFields)) { + Object keyWord = requestParams.get("keyWord"); + if (ObjectUtil.isEmpty(keyWord)) { + // // keyWord值为空,遍历keyWordLikeFields字段,根据queryColumn字段名构建查询条件 + for (QueryColumn queryColumn : keyWordLikeFields) { + String fieldName = queryColumn.getName(); + String paramName = StrUtil.toCamelCase(fieldName); + String paramValue = requestParams.getStr(paramName); + if (ObjectUtil.isNotEmpty(paramValue)) { + queryWrapper.and(queryColumn.like(paramValue)); + } + } + } else { + // keyWord值非空,使用keyWord构建 + // 初始化一个空的 QueryCondition + QueryCondition orCondition = null; + for (QueryColumn queryColumn : keyWordLikeFields) { + QueryCondition condition = queryColumn.like(keyWord); + if (orCondition == null) { + orCondition = condition; + } else { + orCondition = orCondition.or(condition); + } + } + queryWrapper.and(orCondition); + } + } + 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 tableAlias = ""; + List queryTables = (List) ReflectUtil.getFieldValue(queryWrapper, "queryTables"); + if (ObjectUtil.isNotEmpty(queryTables)) { + // 取主表作为排序字段别名 + QueryTable queryTable = queryTables.get(0); + tableAlias = queryTable.getName() + "."; + } + 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( + tableAlias + (tableAnnotation.camelToUnderline() ? StrUtil.toUnderlineCase(order) : order), + sort.equals("asc")); + } + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/request/PageResult.java b/cool-admin-java/src/main/java/com/cool/core/request/PageResult.java new file mode 100644 index 0000000..314a96f --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/request/PageResult.java @@ -0,0 +1,33 @@ +package com.cool.core.request; + +import java.util.List; +import com.mybatisflex.core.paginate.Page; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema( title = "分页数据模型" ) +public class PageResult { + @Schema( title = "分页数据" ) + private List list; + private Pagination pagination = new Pagination(); + + @Data + public static class Pagination { + @Schema( title = "页码" ) + private Long page; + @Schema( title = "本页数量" ) + private Long size; + @Schema( title = "总页数" ) + private Long total; + } + + static public PageResult of(Page page ){ + PageResult result = new PageResult(); + result.setList(page.getRecords()); + result.pagination.setPage( page.getPageNumber() ); + result.pagination.setSize( page.getPageSize() ); + result.pagination.setTotal( page.getTotalRow() ); + return result; + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/request/R.java b/cool-admin-java/src/main/java/com/cool/core/request/R.java new file mode 100644 index 0000000..c152ff9 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/request/R.java @@ -0,0 +1,74 @@ +package com.cool.core.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 返回信息 + */ +@Schema(title = "响应数据结构") +@Data +public class R implements Serializable { + private static final long serialVersionUID = 1L; + + + @Schema(title = "编码:1000表示成功,其他值表示失败") + private int code = 1000; + + @Schema(title = "消息内容") + private String message = "success"; + + @Schema(title = "响应数据") + private T data; + + public R() { + + } + + public R( int code, String message, T data ) { + this.code = code; + this.message = message; + this.data = data; + } + + 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.code = code; + r.message = msg; + return r; + } + + public static R okMsg(String msg) { + R r = new R(); + r.message = msg; + return r; + } + + public static R ok() { + return new R(); + } + + public static R ok(B data) { + return new R(1000 , "success", data); + } + + + public R put(String key, Object value) { + switch (key) { + case "code" -> this.code = (int) value; + case "message" -> this.message = (String) value; + case "data" -> this.data = (T) value; + } + return this; + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/request/RequestParamsFilter.java b/cool-admin-java/src/main/java/com/cool/core/request/RequestParamsFilter.java new file mode 100644 index 0000000..cbcc551 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/request/RequestParamsFilter.java @@ -0,0 +1,114 @@ +package com.cool.core.request; + +import cn.hutool.core.util.ObjUtil; +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.enums.UserTypeEnum; +import com.cool.core.util.BodyReaderHttpServletRequestWrapper; +import com.cool.core.util.CoolSecurityUtil; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +/** + * 封装请求参数 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(); + String language = request.getHeader("language"); + String coolEid = request.getHeader("cool-admin-eid"); + Long tenantId = StrUtil.isEmpty(coolEid) ? null : Long.parseLong(coolEid); + if (StrUtil.isNotEmpty(request.getContentType()) && request.getContentType().contains("multipart/form-data")) { + servletRequest.setAttribute("requestParams", requestParams); + servletRequest.setAttribute("cool-language", language); + servletRequest.setAttribute("tenantId", tenantId); + filterChain.doFilter(servletRequest, servletResponse); + } else { + BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request); + String body = requestWrapper.getBodyString(requestWrapper); + if (StrUtil.isNotEmpty(body) && JSONUtil.isTypeJSON(body) && !JSONUtil.isTypeJSONArray( + body)) { + requestParams = JSONUtil.parseObj(body); + } + Object jwtObj = request.getAttribute("tokenInfo"); + if (jwtObj != null) { + requestParams.set("tokenInfo", ((JWT) jwtObj).getPayload().getClaimsJson()); + } + // 登录状态,设置用户id + Long currTenantId = setUserId(requestParams); + if (ObjUtil.isNotNull(currTenantId)) { + tenantId = currTenantId; + } + requestWrapper.setAttribute("cool-language", language); + request.setAttribute("tenantId", tenantId); + requestParams.set("body", body); + requestParams.putAll(getAllRequestParam(request)); + + requestWrapper.setAttribute("requestParams", requestParams); + filterChain.doFilter(requestWrapper, servletResponse); + } + } + + private Long setUserId(JSONObject requestParams) { + UserTypeEnum userTypeEnum = CoolSecurityUtil.getCurrentUserType(); + switch (userTypeEnum) { + // 只有登录了,才有用户类型, 不然为 UNKNOWN 状态 + case ADMIN -> { + // 管理后台由于之前已经有逻辑再了,怕会影响到,如果自己有传了值不覆盖 + Object o = requestParams.get("userId"); + if (ObjUtil.isNull(o)) { + requestParams.set("userId", CoolSecurityUtil.getCurrentUserId()); + } + } + // app端,userId 为当前登录的用户id + case APP -> requestParams.set("userId", CoolSecurityUtil.getCurrentUserId()); + } + return CoolSecurityUtil.getTenantId(requestParams); + } + + /** + * 获取客户端请求参数中所有的信息 + * + */ + 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/cool-admin-java/src/main/java/com/cool/core/request/RestInterceptor.java b/cool-admin-java/src/main/java/com/cool/core/request/RestInterceptor.java new file mode 100644 index 0000000..21a85cb --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/request/prefix/AutoPrefixConfiguration.java b/cool-admin-java/src/main/java/com/cool/core/request/prefix/AutoPrefixConfiguration.java new file mode 100644 index 0000000..c699a80 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/request/prefix/AutoPrefixUrlMapping.java b/cool-admin-java/src/main/java/com/cool/core/request/prefix/AutoPrefixUrlMapping.java new file mode 100644 index 0000000..01f04ea --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/request/prefix/AutoPrefixUrlMapping.java @@ -0,0 +1,103 @@ +package com.cool.core.request.prefix; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.enums.Apis; +import com.cool.core.util.ConvertUtil; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +/** + * 自动配置模块的路由 + */ +@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(packageName); + String cName = getCName(annotations[0].cname(), 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 = Apis.ALL_API; + 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(String cname, Class handlerType, String prefix) { + if (ObjUtil.isNotEmpty(cname)) { + return cname; + } + String name = handlerType.getName(); + String[] names = name.split("[.]"); + name = names[names.length - 1]; + return ConvertUtil.extractController2Path(ConvertUtil.pathToClassName(prefix), name); + } + + /** + * 构建路由前缀 + * + * @param packageName 包名 + * @return 返回路由前缀 + */ + private String getPrefix(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/cool-admin-java/src/main/java/com/cool/core/security/EntryPointUnauthorizedHandler.java b/cool-admin-java/src/main/java/com/cool/core/security/EntryPointUnauthorizedHandler.java new file mode 100644 index 0000000..1d86f8a --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/security/IgnoredUrlsProperties.java b/cool-admin-java/src/main/java/com/cool/core/security/IgnoredUrlsProperties.java new file mode 100644 index 0000000..2443dae --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/security/IgnoredUrlsProperties.java @@ -0,0 +1,22 @@ +package com.cool.core.security; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 忽略地址配置 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "ignored") +public class IgnoredUrlsProperties { + + // 忽略后台校验权限列表 + private List adminAuthUrls = new ArrayList<>(); + + // 忽略记录请求日志列表 + private List logUrls = new ArrayList<>(); +} diff --git a/cool-admin-java/src/main/java/com/cool/core/security/JwtAuthenticationTokenFilter.java b/cool-admin-java/src/main/java/com/cool/core/security/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..05209df --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/security/JwtAuthenticationTokenFilter.java @@ -0,0 +1,110 @@ +package com.cool.core.security; + +import cn.hutool.core.convert.Convert; +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.enums.UserTypeEnum; +import com.cool.core.security.jwt.JwtTokenUtil; +import com.cool.core.security.jwt.JwtUser; +import com.cool.core.util.PathUtils; +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; + final private IgnoredUrlsProperties ignoredUrlsProperties; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain chain) + throws ServletException, IOException { + String requestURI = request.getRequestURI(); + if (PathUtils.isMatch(ignoredUrlsProperties.getAdminAuthUrls(), requestURI)) { + // 请求路径在忽略后台鉴权url里支持通配符,放行 + chain.doFilter(request, response); + return; + } + String authToken = request.getHeader("Authorization"); + if (!StrUtil.isEmpty(authToken)) { + JWT jwt = jwtTokenUtil.getTokenInfo(authToken); + + Object userType = jwt.getPayload("userType"); + if (Objects.equals(userType, UserTypeEnum.APP.name())) { + // app + handlerAppRequest(request, jwt, authToken); + } else { + // admin + handlerAdminRequest(request, jwt, authToken); + } + } + chain.doFilter(request, response); + } + /** + * 处理app请求 + */ + private void handlerAppRequest(HttpServletRequest request, JWT jwt, String authToken) { + String userId = jwt.getPayload("userId").toString(); + if (ObjectUtil.isNotEmpty(userId) + && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = coolCache.get("app:userDetails:" + userId, + JwtUser.class); + if (jwtTokenUtil.validateToken(authToken) && userDetails != null) { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + request.setAttribute("userId", jwt.getPayload("userId")); + request.setAttribute("tokenInfo", jwt); + } + } + } + + /** + * 处理后台请求 + */ + private void handlerAdminRequest(HttpServletRequest request, JWT jwt, String 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); + } + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/security/JwtSecurityConfig.java b/cool-admin-java/src/main/java/com/cool/core/security/JwtSecurityConfig.java new file mode 100644 index 0000000..6f9e722 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/security/JwtSecurityConfig.java @@ -0,0 +1,152 @@ +package com.cool.core.security; + +import com.cool.core.annotation.TokenIgnore; +import com.cool.core.enums.UserTypeEnum; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotatedElementUtils; +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.FrameOptionsConfig; +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; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.pattern.PathPattern; + +@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; + + final private RequestMappingHandlerMapping requestMappingHandlerMapping; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + // 动态获取忽略的URL + configureIgnoredUrls(); + + return httpSecurity + .authorizeHttpRequests( + conf -> { + conf.requestMatchers( + ignoredUrlsProperties.getAdminAuthUrls().toArray(String[]::new)) + .permitAll(); + conf.requestMatchers("/admin/**").authenticated(); + conf.requestMatchers("/app/**").hasRole(UserTypeEnum.APP.name()); + }) + .headers(config -> config.frameOptions(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(); + } + + private void configureIgnoredUrls() { + Map mappings = requestMappingHandlerMapping.getHandlerMethods(); + List handlerCtr = new ArrayList<>(); + mappings.forEach((requestMappingInfo, handlerMethod) -> { + Method method = handlerMethod.getMethod(); + TokenIgnore tokenIgnore = AnnotatedElementUtils.findMergedAnnotation(method, TokenIgnore.class); + TokenIgnore tokenIgnoreCtr = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), TokenIgnore.class); + if (!handlerCtr.contains(handlerMethod.getBeanType().getName()) && tokenIgnoreCtr != null) { + requestMappingInfo.getPathPatternsCondition().getPatterns().forEach(pathPattern -> { + String[] prefixs = pathPattern.getPatternString().split("/"); + // 去除最后一个路径 + List urls = new ArrayList<>(); + for (int i = 0; i < prefixs.length - 1; i++) { + urls.add(prefixs[i]); + } + // 遍历 tokenIgnoreCtr.value() + for (String path : tokenIgnoreCtr.value()) { + ignoredUrlsProperties.getAdminAuthUrls().add(String.join("/", urls) + "/" + path); + } + if (tokenIgnoreCtr.value().length == 0) { + // 通配 + ignoredUrlsProperties.getAdminAuthUrls().add(String.join("/", urls)+ "/**"); + } + handlerCtr.add(handlerMethod.getBeanType().getName()); + }); + } + if (tokenIgnore != null) { + StringBuilder url = new StringBuilder(); + RequestMapping classRequestMapping = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), RequestMapping.class); + if (classRequestMapping != null) { + for (String path : classRequestMapping.value()) { + url.append(path); + } + } + if (requestMappingInfo.getPathPatternsCondition() == null) { + return; + } + for (PathPattern path : requestMappingInfo.getPathPatternsCondition().getPatterns()) { + url.append(path); + } + ignoredUrlsProperties.getAdminAuthUrls().add(url.toString()); + } + }); + } + + @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/cool-admin-java/src/main/java/com/cool/core/security/MyAccessDecisionManager.java b/cool-admin-java/src/main/java/com/cool/core/security/MyAccessDecisionManager.java new file mode 100644 index 0000000..3c410af --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/security/MyAccessDecisionManager.java @@ -0,0 +1,61 @@ +package com.cool.core.security; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import lombok.RequiredArgsConstructor; +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.security.web.FilterInvocation; +import org.springframework.stereotype.Component; + +/** + * 权限管理决断器 判断用户拥有的权限或角色是否有资源访问权限 + */ +@RequiredArgsConstructor +@Slf4j +@Component +public class MyAccessDecisionManager implements AccessDecisionManager { + // 忽略权限控制的地址 + final private IgnoredUrlsProperties ignoredUrlsProperties; + + @Override + public void decide(Authentication authentication, Object o, Collection configAttributes) + throws AccessDeniedException, InsufficientAuthenticationException { + if (configAttributes == null) { + return; + } + List urls = ignoredUrlsProperties.getAdminAuthUrls(); + String url = ((FilterInvocation) o).getRequestUrl().split("[?]")[0]; + if (urls.contains(url)) { + 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; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/security/MyFilterSecurityInterceptor.java b/cool-admin-java/src/main/java/com/cool/core/security/MyFilterSecurityInterceptor.java new file mode 100644 index 0000000..f22165b --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/security/RestAccessDeniedHandler.java b/cool-admin-java/src/main/java/com/cool/core/security/RestAccessDeniedHandler.java new file mode 100644 index 0000000..7954d05 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/security/jwt/JwtTokenUtil.java b/cool-admin-java/src/main/java/com/cool/core/security/jwt/JwtTokenUtil.java new file mode 100644 index 0000000..5335726 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/security/jwt/JwtTokenUtil.java @@ -0,0 +1,165 @@ +package com.cool.core.security.jwt; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +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 com.cool.modules.base.service.sys.BaseSysConfService; +import java.io.Serializable; +import java.util.Date; +import java.util.Map; +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 tokenKey = "JWT_SECRET_TOKEN"; + final String refreshTokenKey = "JWT_SECRET_REFRESH_TOKEN"; + + public long getExpire() { + return this.coolProperties.getToken().getExpire(); + } + + public long getRefreshExpire() { + return this.coolProperties.getToken().getRefreshExpire(); + } + + public String getTokenSecret() { + String secret = baseSysConfService.getValueWithCache(tokenKey); + if (StrUtil.isBlank(secret)) { + secret = StrUtil.uuid().replaceAll("-", ""); + baseSysConfService.setValue(tokenKey, secret); + } + return secret; + } + + public String getRefreshTokenSecret() { + String secret = baseSysConfService.getValueWithCache(refreshTokenKey); + if (StrUtil.isBlank(secret)) { + secret = StrUtil.uuid().replaceAll("-", ""); + baseSysConfService.setValue(refreshTokenKey, 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(getTokenSecret().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(getRefreshTokenSecret().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) { + if (ObjectUtil.isEmpty(token)) { + return false; + } + String tokenUsername = getUsernameFromToken(token); + String secret = getTokenSecret(); + boolean isValidSignature = JWTUtil.verify(token, secret.getBytes()); + return (tokenUsername.equals(username) && !isTokenExpired(token) && isValidSignature); + } + + /** + * 校验token是否有效 + * @param token + * @return + */ + public Boolean validateToken(String token) { + if (ObjectUtil.isEmpty(token)) { + return false; + } + String secret = getTokenSecret(); + boolean isValidSignature = JWTUtil.verify(token, secret.getBytes()); + return (!isTokenExpired(token) && isValidSignature); + } + + /** + * 校验refresh token是否有效 + * @param token + * @return + */ + public Boolean validateRefreshToken(String token) { + if (ObjectUtil.isEmpty(token)) { + return false; + } + String secret = getRefreshTokenSecret(); + boolean isValidSignature = JWTUtil.verify(token, secret.getBytes()); + return (!isTokenExpired(token) && isValidSignature); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/security/jwt/JwtUser.java b/cool-admin-java/src/main/java/com/cool/core/security/jwt/JwtUser.java new file mode 100644 index 0000000..c69cf9e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/security/jwt/JwtUser.java @@ -0,0 +1,78 @@ +package com.cool.core.security.jwt; + +import com.cool.core.enums.UserTypeEnum; +import java.util.Collection; +import java.util.List; +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * 后台用户信息 + */ +@Data +public class JwtUser implements UserDetails { + + /****** + * 后台用户 + * ********/ + private Long userId; + private String username; + private String password; + private Boolean status; + private UserTypeEnum userTypeEnum; + private List perms; + public JwtUser(Long userId, String username, String password, List perms, Boolean status) { + this.userId = userId; + this.username = username; + this.password = password; + this.perms = perms; + this.status = status; + this.userTypeEnum = UserTypeEnum.ADMIN; + } + + /****** + * app用户 + * ********/ + public JwtUser(Long userId, List perms, Boolean status) { + this.userId = userId; + this.perms = perms; + this.status = status; + this.userTypeEnum = UserTypeEnum.APP; + } + + @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/cool-admin-java/src/main/java/com/cool/core/tenant/CoolTenantFactory.java b/cool-admin-java/src/main/java/com/cool/core/tenant/CoolTenantFactory.java new file mode 100644 index 0000000..f79ebef --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/tenant/CoolTenantFactory.java @@ -0,0 +1,14 @@ +package com.cool.core.tenant; + +import com.cool.core.util.TenantUtil; +import com.mybatisflex.core.tenant.TenantFactory; + +public class CoolTenantFactory implements TenantFactory { + public Object[] getTenantIds(){ + Long tenantId = TenantUtil.getTenantId(); + if (tenantId == null) { + return null; + } + return new Object[]{tenantId}; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/util/AnnotationUtils.java b/cool-admin-java/src/main/java/com/cool/core/util/AnnotationUtils.java new file mode 100644 index 0000000..b65d6a3 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/AnnotationUtils.java @@ -0,0 +1,73 @@ +package com.cool.core.util; + +import com.cool.core.annotation.CoolPlugin; +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +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; + +@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 { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + if (clazz.getAnnotation( + (Class) contextClassLoader.loadClass(CoolPlugin.class.getName())) != null) { + return true; + } + } catch (Exception e) { + log.error("出现异常:{}", e.getMessage(), e); + } + return false; + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/util/AutoTypeConverter.java b/cool-admin-java/src/main/java/com/cool/core/util/AutoTypeConverter.java new file mode 100644 index 0000000..45ed11c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/AutoTypeConverter.java @@ -0,0 +1,27 @@ +package com.cool.core.util; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.NumberUtil; + +import java.io.Serializable; + +public class AutoTypeConverter { + /** + * 将字符串自动转换为数字或保留为字符串 + * + * @param input 输入字符串 + * @return Integer / Long / String + */ + public static Serializable autoConvert(Object input) { + if (input == null) { + return null; + } + if (NumberUtil.isInteger(input.toString())) { + return Convert.convert(Integer.class, input); + } else if (NumberUtil.isLong(input.toString())) { + return Convert.convert(Long.class, input); + } else { + return (Serializable) input; + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/util/BodyReaderHttpServletRequestWrapper.java b/cool-admin-java/src/main/java/com/cool/core/util/BodyReaderHttpServletRequestWrapper.java new file mode 100644 index 0000000..2c783d2 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/util/CompilerUtils.java b/cool-admin-java/src/main/java/com/cool/core/util/CompilerUtils.java new file mode 100644 index 0000000..3e6506f --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/CompilerUtils.java @@ -0,0 +1,180 @@ +package com.cool.core.util; + +import cn.hutool.core.io.FileUtil; +import com.mybatisflex.processor.MybatisFlexProcessor; +import java.io.File; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.nio.charset.StandardCharsets; +import java.util.List; +import javax.annotation.processing.Processor; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CompilerUtils { + + public final static String META_INF_VERSIONS = "META-INF/versions/"; + + // jdk版本 + private static String JVM_VERSION = null; + + /** + * 获取jdk版本 + */ + public static String getJdkVersion() { + if (JVM_VERSION == null) { + RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); + JVM_VERSION = runtimeMXBean.getSpecVersion(); + } + return JVM_VERSION; + } + + /** + * 创建文件, 先删除在创建 + */ + public static void createFile(String content, String filePathStr) { + FileUtil.del(filePathStr); + File file = FileUtil.touch(filePathStr); + FileUtil.appendString(content, file, StandardCharsets.UTF_8.name()); + compileAndSave(filePathStr); + } + + public static String createMapper(String actModulePath, String fileName, String mapper) { + String pathStr = actModulePath + File.separator + "mapper" + File.separator; + String filePathStr = pathStr + fileName + "Mapper.java"; + createFile(mapper, filePathStr); + return filePathStr; + } + + public static String createServiceImpl(String actModulePath, String fileName, + String serviceImpl) { + String pathStr = actModulePath + File.separator + "service" + File.separator + "impl" + File.separator; + String filePathStr = pathStr + fileName + "ServiceImpl.java"; + createFile(serviceImpl, filePathStr); + return filePathStr; + } + + public static String createService(String actModulePath, String fileName, String service) { + String pathStr = actModulePath + File.separator + "service" + File.separator; + String filePathStr = pathStr + fileName + "Service.java"; + createFile(service, filePathStr); + return filePathStr; + } + + public static String createEntity(String actModulePath, String fileName, String entity) { + String pathStr = actModulePath + File.separator + "entity" + File.separator; + String filePathStr = pathStr + fileName + "Entity.java"; + createFile(entity, filePathStr); + return filePathStr; + } + + public static String createController(String actModulePath, String fileName, String controller) { + String pathStr = actModulePath + File.separator + "controller" + File.separator + "admin" + File.separator; + String filePathStr = pathStr + "Admin" + fileName + "Controller.java"; + createFile(controller, filePathStr); + return filePathStr; + } + + public static String createModule(String modulesPath, String module) { + String pathStr = modulesPath + File.separator + module; + PathUtils.noExistsMk(pathStr); + return pathStr; + } + + public static boolean compileAndSave(String sourceFile) { + // 获取系统 Java 编译器 + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + + // 获取标准文件管理器 + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) { + + // 设置编译输出目录 + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(new File("target" + File.separator + "classes"))); + + // 获取源文件 + List javaFiles = List.of(new File(sourceFile)); + Iterable compilationUnits = fileManager.getJavaFileObjectsFromFiles(javaFiles); + + // 创建编译任务 + JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits); + // 执行编译任务 + return task.call(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + public static void compilerEntityTableDef(String actModulePath, String fileName, String entityPath, List javaPathList) { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) { + Iterable compilationUnits = fileManager.getJavaFileObjects( + entityPath); + // 设置注解处理器 + Iterable processors = List.of(new MybatisFlexProcessor()); + // 添加 -proc:only 选项 + List options = List.of("-proc:only"); + JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options, + null, compilationUnits); + task.setProcessors(processors); + task.call(); + compilationUnits = fileManager.getJavaFileObjects( + javaPathList.toArray(new String[0])); + // 设置编译输出目录 + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(new File("target/classes"))); + + task = compiler.getTask(null, fileManager, null, null, null, compilationUnits); + String pathStr = actModulePath + File.separator + "entity" + File.separator + "table" + File.separator; + String filePathStr = pathStr + fileName + "EntityTableDef.java"; + // 需在entity之后加载 + javaPathList.add(1, filePathStr); + boolean success = task.call(); + if (success) { + System.out.println("Compilation and annotation processing completed successfully."); + // 指定源文件夹和目标文件夹 + File sourceDir = new File("com"); + File destinationDir = new File(PathUtils.getTargetGeneratedAnnotations()); + // 确保目标文件夹存在 + destinationDir.mkdirs(); + // 移动源文件夹内容到目标文件夹 + if (sourceDir.exists()) { + FileUtil.move(sourceDir, destinationDir, true); + } + if (countFiles(sourceDir) <= 1) { + FileUtil.clean(sourceDir); + FileUtil.del(sourceDir); + } + } else { + System.out.println("Compilation and annotation processing failed."); + } + } catch (IOException e) { + log.error("compilerEntityTableDefError", e); + } + } + private static int countFiles(File directory) { + File[] files = directory.listFiles(); + if (files == null) { + return 0; + } + + int count = 0; + for (File file : files) { + if (file.isFile()) { + count++; + } else if (file.isDirectory()) { + count += countFiles(file); + } + // If more than one file is found, no need to continue counting + if (count > 1) { + break; + } + } + return count; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/util/ConvertUtil.java b/cool-admin-java/src/main/java/com/cool/core/util/ConvertUtil.java new file mode 100644 index 0000000..be72f86 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/ConvertUtil.java @@ -0,0 +1,257 @@ +package com.cool.core.util; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.*; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +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(); + } + } + } + } + + + + /** + * /admin/goods 转 AdminGoods + */ + public static String pathToClassName(String path) { + // 按斜杠分割字符串 + String[] parts = path.split("/"); + StringBuilder className = new StringBuilder(); + for (String part : parts) { + // 将每个部分的首字母大写,并追加到 StringBuilder 中 + className.append(StrUtil.upperFirst(part)); + } + return className.toString(); + } + + public static String extractController2Path(String prefix, String className) { + Pattern pattern = Pattern.compile("([A-Za-z0-9]+)Controller$"); + Matcher matcher = pattern.matcher(className); + + if (matcher.find()) { + String extracted = matcher.group(1); + + // 将前缀拆分为单词数组 + String[] prefixWords = splitCamelCase(prefix); + String[] classWords = splitCamelCase(extracted); + + // 从前缀和类名中逐个匹配并去除匹配的部分 + int i = 0; + for (int j = 0; i < prefixWords.length; j++) { + if (j >= classWords.length) { + break; + } + for (String prefixWord : prefixWords) { + if (prefixWord.equalsIgnoreCase(classWords[i])) { + i++; + break; + } + } + } + // 从当前位置开始,拼接剩余部分 + return String.join("/", java.util.Arrays.copyOfRange(classWords, i, classWords.length)).toLowerCase(); + } + return ""; + } + + // 拆分驼峰命名的字符串为单词数组 + private static String[] splitCamelCase(String input) { + return input.split("(?<=.)(?=[A-Z])"); + } + + /** + * 将给定的字段值转换为可序列化的形式 + * 此方法旨在将一个对象的特定字段值转换为其相应的可序列化类型 + * 它在序列化和反序列化过程中特别有用,确保字段值可以被正确处理 + * + * @param fieldName 字段名称,用于查找字段类型 + * @param fieldValue 待转换的字段值 + * @param clazz 包含该字段的类 + * @return 转换后的可序列化字段值,如果无法确定字段类型,则返回原始值 + */ + public static Object convertByClass(String fieldName, Object fieldValue, Class clazz) { + // 检查输入参数是否为空,如果字段名或字段值为空,则直接返回字段值 + if (fieldName == null || fieldValue == null) { + return fieldValue; + } + + // 获取字段类型 + Class fieldType = getFieldType(clazz, fieldName); + // 如果字段类型为空,则直接返回字段值 + if (fieldType == null) { + return fieldValue; + } + + // 使用Convert类的convert方法将字段值转换为字段类型 + return Convert.convert(fieldType, fieldValue); + } + + public static List covertListByClass(String fieldName, List fieldValue, Class clazz) { + // 检查输入参数是否为空,如果字段名或字段值为空,则直接返回字段值 + if (fieldName == null || fieldValue == null) { + return fieldValue; + } + + // 获取字段类型 + Class fieldType = getFieldType(clazz, fieldName); + // 如果字段类型为空,则直接返回字段值 + if (fieldType == null) { + return fieldValue; + } + + return Collections.singletonList(Convert.toList(fieldType, fieldValue)); + } + /** + * 获取指定类中指定字段的类型 + * + * @param clazz 目标类 + * @param fieldName 字段名称 + * @return 字段的类型 Class,如果字段不存在则返回 null + */ + public static Class getFieldType(Class clazz, String fieldName) { + Field field = ReflectUtil.getField(clazz, fieldName); + return field != null ? field.getType() : null; + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/util/CoolPluginInvokers.java b/cool-admin-java/src/main/java/com/cool/core/util/CoolPluginInvokers.java new file mode 100644 index 0000000..2a2fdf2 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/util/CoolSecurityUtil.java b/cool-admin-java/src/main/java/com/cool/core/util/CoolSecurityUtil.java new file mode 100644 index 0000000..2dc6576 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/CoolSecurityUtil.java @@ -0,0 +1,116 @@ +package com.cool.core.util; + +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import com.cool.core.cache.CoolCache; +import com.cool.core.enums.UserTypeEnum; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.security.jwt.JwtUser; +import com.cool.modules.base.entity.sys.BaseSysUserEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * Security 工具类 + */ +public class CoolSecurityUtil { + + private static final CoolCache coolCache = SpringUtil.getBean(CoolCache.class); + + /***************后台********************/ + /** + * 获取后台登录的用户名 + */ + public static String getAdminUsername() { + return SecurityContextHolder.getContext().getAuthentication().getName(); + } + + /** + * 是否为超级管理员 + */ + public static boolean isSuperAdmin() { + return "admin".equals(getAdminUsername()); + } + /** + * 获得jwt中的信息 + * + * @param requestParams 请求参数 + * @return jwt + */ + public static JSONObject getAdminUserInfo(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; + } + + public static Long getTenantId(JSONObject requestParams) { + JSONObject tokenInfo = requestParams.getJSONObject("tokenInfo"); + if (tokenInfo != null) { + return tokenInfo.getLong("tenantId"); + } + return null; + } + + /** + * 后台账号退出登录 + * + * @param adminUserId 用户ID + * @param username 用户名 + */ + public static void adminLogout(Long adminUserId, String username) { + coolCache.del("admin:department:" + adminUserId, "admin:passwordVersion:" + adminUserId, + "admin:userInfo:" + adminUserId, "admin:userDetails:" + username); + } + + /** + * 后台账号退出登录 + * + * @param userEntity 用户 + */ + public static void adminLogout(BaseSysUserEntity userEntity) { + adminLogout(userEntity.getId(), userEntity.getUsername()); + } + + + /** + * 获取当前用户id + */ + public static Long getCurrentUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof UserDetails) { + return ((JwtUser) principal).getUserId(); + } + } + CoolPreconditions.check(true, 401, "未登录"); + return null; + } + + /** + * 获取当前用户类型 + */ + public static UserTypeEnum getCurrentUserType() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof UserDetails) { + return ((JwtUser) principal).getUserTypeEnum(); + } + } + // 还未登录,未知类型 + return UserTypeEnum.UNKNOWN; + } + + /** + * app退出登录,移除缓存信息 + */ + public static void appLogout() { + coolCache.del("app:userDetails"+ getCurrentUserId()); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/util/DatabaseDialectUtils.java b/cool-admin-java/src/main/java/com/cool/core/util/DatabaseDialectUtils.java new file mode 100644 index 0000000..f4f2554 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/DatabaseDialectUtils.java @@ -0,0 +1,56 @@ +package com.cool.core.util; +import com.cool.core.exception.CoolPreconditions; +import org.dromara.autotable.core.constants.DatabaseDialect; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import javax.sql.DataSource; + +/** + * 获取数据库方言 + */ +public class DatabaseDialectUtils { + private static String dialect; + + public static String getDatabaseDialect(DataSource dataSource) { + if (dialect == null) { + dialect = determineDatabaseType(dataSource); + } + return dialect; + } + + public static boolean isPostgresql() { + DataSource dataSource = SpringContextUtils.getBean(DataSource.class); + return DatabaseDialect.PostgreSQL.equals(getDatabaseDialect(dataSource)); + } + + public static boolean isPostgresql(DataSource dataSource) { + return DatabaseDialect.PostgreSQL.equals(getDatabaseDialect(dataSource)); + } + + + private static String determineDatabaseType(DataSource dataSource) { + // 从 DataSource 获取连接 + try (Connection connection = dataSource.getConnection()) { + // 获取元数据 + DatabaseMetaData metaData = connection.getMetaData(); + String productName = metaData.getDatabaseProductName(); + + return inferDatabaseTypeFromProductName(productName); + } catch (SQLException e) { + throw new RuntimeException("Failed to determine database dialect", e); + } + } + + private static String inferDatabaseTypeFromProductName(String productName) { + if (productName.startsWith(DatabaseDialect.MySQL)) { + return DatabaseDialect.MySQL; + } else if (productName.startsWith(DatabaseDialect.PostgreSQL)) { + return DatabaseDialect.PostgreSQL; + } else if (productName.startsWith(DatabaseDialect.SQLite)) { + return DatabaseDialect.SQLite; + } + CoolPreconditions.alwaysThrow("暂不支持!"); + return "unknown"; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/util/EntityUtils.java b/cool-admin-java/src/main/java/com/cool/core/util/EntityUtils.java new file mode 100644 index 0000000..b564a0c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/EntityUtils.java @@ -0,0 +1,121 @@ +package com.cool.core.util; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.util.ObjUtil; +import com.mybatisflex.annotation.Table; +import com.mybatisflex.core.query.QueryColumn; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +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/**/entity/**/*Entity.class"); + for (Resource r : resources) { + String path = r.getURL().getPath(); + String className = path.substring(path.indexOf("com/cool"), + 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) { + ArrayList excludeList = new ArrayList<>(List.of(excludeNames)); + return Arrays.stream(queryColumns).toList().stream() + .filter(o -> !excludeList.contains(o.getName())).toList(); + } + + /** + * 将bean的部分属性转换成map
+ * 可选拷贝哪些属性值,默认是不忽略值为{@code null}的值的。 + * + * @param bean bean + * @param ignoreProperties 需要忽略拷贝的属性值,{@code null}或空表示拷贝所有值 + * @return Map + * @since 5.8.0 + */ + public static Map toMap(Object bean, String... ignoreProperties) { + int mapSize = 16; + Editor keyEditor = null; + final Set propertiesSet = CollUtil.set(false, ignoreProperties); + propertiesSet.add("queryWrapper"); + mapSize = ignoreProperties.length; + keyEditor = property -> !propertiesSet.contains(property) ? property : null; + // 指明了要复制的属性 所以不忽略null值 + return BeanUtil.beanToMap(bean, new LinkedHashMap<>(mapSize, 1), false, keyEditor); + } + + /** + * 检查字段名是否在排除列表中 + * + * @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/cool-admin-java/src/main/java/com/cool/core/util/I18nUtil.java b/cool-admin-java/src/main/java/com/cool/core/util/I18nUtil.java new file mode 100644 index 0000000..13d28d5 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/I18nUtil.java @@ -0,0 +1,125 @@ +package com.cool.core.util; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +@Slf4j +@Component +public class I18nUtil { + + public static final String MSG_PREFIX = "msg_"; + public static final String MENU_PREFIX = "menu_"; + public static final String DICT_INFO_PREFIX = "dictInfo_"; + public static final String DICT_TYPE_PREFIX = "dictType_"; + + public static boolean enable = false; + + public static String path; + public static String getLanguage() { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + return null; + } + return (String) attributes.getAttribute("cool-language", RequestAttributes.SCOPE_REQUEST); + } + + private static final Map data = new ConcurrentHashMap<>(); + + private void load(String key, File file) { + try { + String content = FileUtil.readUtf8String(file); + data.put(key, JSONUtil.parseObj(content)); + } catch (Exception e) { + log.error("读取国际化文件失败", e); + } + } + + public boolean exist(String name) { + // 获取该目录下所有的 .json 文件 + List jsonFiles = FileUtil.loopFiles(getPath(), file -> + file.isFile() && file.getName().endsWith(".json") + ); + AtomicReference flag = new AtomicReference<>(false); + jsonFiles.forEach(file -> { + String parentName = file.getParentFile().getName(); + String key = parentName + "_" + file.getName().replace(".json", ""); + if (key.equals(name)) { + flag.set(true); + // 加载 + load(key, file); + } + }); + return flag.get(); + } + + public static String getI18nMenu(String name) { + return getI18n(name, MENU_PREFIX); + } + + public static String getI18nMsg(String name) { + return getI18n(name, MSG_PREFIX); + } + + public static String getI18nDictInfo(String name) { + return getI18n(name, DICT_INFO_PREFIX); + } + public static String getI18nDictType(String name) { + return getI18n(name, DICT_TYPE_PREFIX); + } + private static String getI18n(String name, String prefix) { + if (!enable) { + return name; + } + String language = I18nUtil.getLanguage(); + if (language == null) { + return name; + } + JSONObject jsonObject = data.get(prefix + language); + if (jsonObject == null) { + return name; + } + String str = jsonObject.getStr(name); + if (str == null) { + return name; + } + return str; + } + + public void update(String key, JSONObject object) { + data.put(key, object); + String[] split = key.split("_"); + String absolutePath = getPath(); + File file = FileUtil.file(absolutePath, split[0], split[1] + ".json"); + // 确保父目录存在 + FileUtil.mkParentDirs(file); + // 写入内容 + FileUtil.writeUtf8String(JSONUtil.toJsonStr(object), file); + } + + private String getPath() { + String absolutePath = path; + if (!PathUtils.isAbsolutePath(absolutePath)) { + absolutePath = PathUtils.getUserDir() + File.separator + absolutePath; + } + return absolutePath; + } + + public void clear() { + data.clear(); + List jsonFiles = FileUtil.loopFiles(getPath(), file -> + file.isFile() && file.getName().endsWith(".json") + ); + jsonFiles.forEach(File::delete); + enable = false; + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/core/util/IPUtils.java b/cool-admin-java/src/main/java/com/cool/core/util/IPUtils.java new file mode 100644 index 0000000..ed33d80 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/util/MapExtUtil.java b/cool-admin-java/src/main/java/com/cool/core/util/MapExtUtil.java new file mode 100644 index 0000000..3179ea2 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/util/MappingAlgorithm.java b/cool-admin-java/src/main/java/com/cool/core/util/MappingAlgorithm.java new file mode 100644 index 0000000..783aa77 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/MappingAlgorithm.java @@ -0,0 +1,21 @@ +package com.cool.core.util; + +/** + * 自定义映射算法 + * 将 ID 转换为一个混淆形式的数字,然后能够逆向转换回原始 ID。 + * 场景:混淆订单id + */ +public class MappingAlgorithm { + + private static final long ENCRYPTION_KEY = 123456789L; // 任意密钥 + + // 将 ID 转换为混淆的数字 + public static long encrypt(long id) { + return id ^ ENCRYPTION_KEY; // 使用异或操作进行混淆 + } + + // 将混淆的数字恢复为原始的 ID + public static long decrypt(long encryptedId) { + return encryptedId ^ ENCRYPTION_KEY; // 逆操作恢复原始 ID + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/util/PathUtils.java b/cool-admin-java/src/main/java/com/cool/core/util/PathUtils.java new file mode 100644 index 0000000..0f61184 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/PathUtils.java @@ -0,0 +1,70 @@ +package com.cool.core.util; + +import cn.hutool.core.io.file.PathUtil; +import cn.hutool.core.text.AntPathMatcher; +import com.cool.CoolApplication; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +public class PathUtils { + private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); + + 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() + getSrcMainJava() + File.separator + CoolApplication.class.getPackageName() + .replace(".", File.separator) + File.separator + "modules"; + } + + public static String getSrcMainJava() { + return File.separator + "src" + File.separator + "main" + File.separator + "java"; + } + public static String getTargetGeneratedAnnotations() { + return "target" + File.separator + "generated-sources" + File.separator + "annotations"; + } + + public static String getClassName(String filePath) { + // 定位 "/src/main/java" 在路径中的位置 + int srcMainJavaIndex = filePath.indexOf(getSrcMainJava()); + if (srcMainJavaIndex == -1) { + throw new IllegalArgumentException("File path does not contain 'src/main/java'"); + } + + // 提取 "src/main/java" 之后的路径 + // 将文件分隔符替换为包分隔符 + return filePath.substring(srcMainJavaIndex + ("src" + File.separator + "main" + File.separator + "java").length() + 2) + .replace(File.separator, ".").replace(".java", ""); + } + + /** + * 路径不存在创建 + */ + public static void noExistsMk(String pathStr) { + Path path = Paths.get(pathStr); + if (PathUtil.exists(path, false)) { + PathUtil.mkParentDirs(path); + } + } + + /** + * 判断给定的请求URI是否匹配列表中的任意一个URL模式 + * 使用Ant风格的路径匹配来处理URL模式,提供了一种通配符匹配的方法 + * + * @param urls 待匹配的URL模式列表 + * @param requestURI 请求的URI + * @return 如果请求URI匹配列表中的任意一个URL模式,则返回true;否则返回false + */ + public static boolean isMatch(List urls, String requestURI) { + return urls.stream() + .anyMatch(url -> antPathMatcher.match(url, requestURI)); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/core/util/SpringContextUtils.java b/cool-admin-java/src/main/java/com/cool/core/util/SpringContextUtils.java new file mode 100644 index 0000000..f026531 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/core/util/TenantUtil.java b/cool-admin-java/src/main/java/com/cool/core/util/TenantUtil.java new file mode 100644 index 0000000..66d20e4 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/core/util/TenantUtil.java @@ -0,0 +1,14 @@ +package com.cool.core.util; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +public class TenantUtil { + public static Long getTenantId() { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + return null; + } + return (Long) attributes.getAttribute("tenantId", RequestAttributes.SCOPE_REQUEST); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/controller/admin/AdminAiApiConfigController.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/controller/admin/AdminAiApiConfigController.java new file mode 100644 index 0000000..8d7ae3f --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/controller/admin/AdminAiApiConfigController.java @@ -0,0 +1,44 @@ +package com.cool.modules.aiconfig.controller.admin; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.aiconfig.entity.AiApiConfigEntity; +import com.cool.modules.aiconfig.service.AiApiConfigService; +import com.cool.core.request.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import static com.cool.modules.aiconfig.entity.table.AiApiConfigEntityTableDef.AI_API_CONFIG_ENTITY; + +/** + * AI模型接口配置管理 + */ +@Tag(name = "AI接口配置", description = "AI模型接口配置管理") +@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}) +public class AdminAiApiConfigController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption(createOp() + .keyWordLikeFields(AI_API_CONFIG_ENTITY.API_URL) + .fieldEq(AI_API_CONFIG_ENTITY.MODEL_ID) + ); + } + + @Operation(summary = "测试接口连通性", description = "测试AI接口是否可以正常连接") + @PostMapping("/testConnection") + public R testConnection(@RequestBody JSONObject body) { + Long id = body.getLong("id"); + boolean result = this.service.testConnection(id); + + if (result) { + return R.ok("接口连接测试成功"); + } else { + return R.error("接口连接测试失败,请检查配置"); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/controller/admin/AdminAiModelController.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/controller/admin/AdminAiModelController.java new file mode 100644 index 0000000..4621971 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/controller/admin/AdminAiModelController.java @@ -0,0 +1,35 @@ +package com.cool.modules.aiconfig.controller.admin; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.aiconfig.entity.AiModelEntity; +import com.cool.modules.aiconfig.service.AiModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.GetMapping; +import com.cool.core.request.R; + +import static com.cool.modules.aiconfig.entity.table.AiModelEntityTableDef.AI_MODEL_ENTITY; + +/** + * AI模型配置管理 + */ +@Tag(name = "AI模型配置", description = "AI模型配置管理") +@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}) +public class AdminAiModelController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption(createOp() + .keyWordLikeFields(AI_MODEL_ENTITY.MODEL_NAME, AI_MODEL_ENTITY.MODEL_KEY, AI_MODEL_ENTITY.MODEL_TYPE) + ); + } + + @Operation(summary = "获取启用的模型列表", description = "获取所有状态为启用的模型列表") + @GetMapping("/enabled") + public R getEnabledModels() { + return R.ok(this.service.getEnabledModels()); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/controller/admin/AdminAiTemplateController.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/controller/admin/AdminAiTemplateController.java new file mode 100644 index 0000000..9a00f4c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/controller/admin/AdminAiTemplateController.java @@ -0,0 +1,43 @@ +package com.cool.modules.aiconfig.controller.admin; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.modules.aiconfig.entity.AiTemplateEntity; +import com.cool.modules.aiconfig.service.AiTemplateService; +import com.cool.core.request.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import static com.cool.modules.aiconfig.entity.table.AiTemplateEntityTableDef.AI_TEMPLATE_ENTITY; + +/** + * AI模板提示词配置管理 + */ +@Tag(name = "AI模板配置", description = "AI模板提示词配置管理") +@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}) +public class AdminAiTemplateController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption(createOp() + .keyWordLikeFields(AI_TEMPLATE_ENTITY.TEMPLATE_NAME, AI_TEMPLATE_ENTITY.TEMPLATE_KEY, AI_TEMPLATE_ENTITY.SCENE) + .fieldEq(AI_TEMPLATE_ENTITY.MODEL_ID, AI_TEMPLATE_ENTITY.SCENE) + ); + } + + @Operation(summary = "根据场景获取模板列表", description = "根据使用场景获取对应的模板列表") + @GetMapping("/byScene") + public R getByScene(@RequestParam String scene) { + return R.ok(this.service.getByScene(scene)); + } + + @Operation(summary = "根据模型ID获取模板列表", description = "根据模型ID获取对应的模板列表") + @GetMapping("/byModel") + public R getByModel(@RequestParam Long modelId) { + return R.ok(this.service.getByModelId(modelId)); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/entity/AiApiConfigEntity.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/entity/AiApiConfigEntity.java new file mode 100644 index 0000000..0e2b089 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/entity/AiApiConfigEntity.java @@ -0,0 +1,49 @@ +package com.cool.modules.aiconfig.entity; + +import com.cool.core.base.BaseEntity; +import com.cool.core.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +/** + * AI模型接口配置实体类 + */ +@Getter +@Setter +@Table(value = "ai_api_config", comment = "AI模型接口配置表") +public class AiApiConfigEntity extends BaseEntity { + + @ColumnDefine(comment = "关联模型ID", notNull = true) + private Long modelId; + + @ColumnDefine(comment = "接口地址", notNull = true, length = 500) + private String apiUrl; + + @ColumnDefine(comment = "API密钥", length = 500) + private String apiKey; + + @ColumnDefine(comment = "API密钥名称(如:Authorization、api-key等)", length = 100) + private String apiKeyName; + + @ColumnDefine(comment = "请求方法(GET/POST等)", defaultValue = "POST") + private String requestMethod; + + @ColumnDefine(comment = "请求头配置(JSON格式)", type = "text") + private String requestHeaders; + + @ColumnDefine(comment = "请求体格式(JSON格式)", type = "text") + private String requestBodyFormat; + + @ColumnDefine(comment = "响应数据解析路径(JSONPath格式)", length = 500) + private String responseDataPath; + + @ColumnDefine(comment = "超时时间(秒)", defaultValue = "60") + private Integer timeout; + + @ColumnDefine(comment = "状态 0-禁用 1-启用", defaultValue = "1") + private Integer status; + + @ColumnDefine(comment = "备注") + private String remark; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/entity/AiModelEntity.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/entity/AiModelEntity.java new file mode 100644 index 0000000..2c28ab2 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/entity/AiModelEntity.java @@ -0,0 +1,36 @@ +package com.cool.modules.aiconfig.entity; + +import com.cool.core.base.BaseEntity; +import com.cool.core.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import com.tangzc.mybatisflex.autotable.annotation.UniIndex; +import lombok.Getter; +import lombok.Setter; + +/** + * AI模型配置实体类 + */ +@Getter +@Setter +@Table(value = "ai_model", comment = "AI模型配置表") +public class AiModelEntity extends BaseEntity { + + @UniIndex + @ColumnDefine(comment = "模型标识(唯一)", notNull = true) + private String modelKey; + + @ColumnDefine(comment = "模型名称", notNull = true) + private String modelName; + + @ColumnDefine(comment = "模型描述") + private String description; + + @ColumnDefine(comment = "模型类型(如:openai、qwen、zhipu等)", notNull = true) + private String modelType; + + @ColumnDefine(comment = "状态 0-禁用 1-启用", defaultValue = "1") + private Integer status; + + @ColumnDefine(comment = "排序号", defaultValue = "0") + private Integer orderNum; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/entity/AiTemplateEntity.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/entity/AiTemplateEntity.java new file mode 100644 index 0000000..bdd321d --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/entity/AiTemplateEntity.java @@ -0,0 +1,51 @@ +package com.cool.modules.aiconfig.entity; + +import com.cool.core.base.BaseEntity; +import com.cool.core.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import com.tangzc.mybatisflex.autotable.annotation.UniIndex; +import lombok.Getter; +import lombok.Setter; + +/** + * AI模板提示词配置实体类 + */ +@Getter +@Setter +@Table(value = "ai_template", comment = "AI模板提示词配置表") +public class AiTemplateEntity extends BaseEntity { + + @UniIndex + @ColumnDefine(comment = "模板标识(唯一)", notNull = true) + private String templateKey; + + @ColumnDefine(comment = "模板名称", notNull = true) + private String templateName; + + @ColumnDefine(comment = "关联模型ID", notNull = true) + private Long modelId; + + @ColumnDefine(comment = "使用场景(如:日报生成、周报生成、月报生成、工作建议等)", notNull = true) + private String scene; + + @ColumnDefine(comment = "提示词内容", type = "text", notNull = true) + private String promptContent; + + @ColumnDefine(comment = "系统提示词", type = "text") + private String systemPrompt; + + @ColumnDefine(comment = "温度参数(0-1之间)", defaultValue = "0.7") + private Double temperature; + + @ColumnDefine(comment = "最大token数", defaultValue = "2000") + private Integer maxTokens; + + @ColumnDefine(comment = "状态 0-禁用 1-启用", defaultValue = "1") + private Integer status; + + @ColumnDefine(comment = "排序号", defaultValue = "0") + private Integer orderNum; + + @ColumnDefine(comment = "备注") + private String remark; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/mapper/AiApiConfigMapper.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/mapper/AiApiConfigMapper.java new file mode 100644 index 0000000..18b9f7a --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/mapper/AiApiConfigMapper.java @@ -0,0 +1,12 @@ +package com.cool.modules.aiconfig.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.aiconfig.entity.AiApiConfigEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI模型接口配置Mapper + */ +@Mapper +public interface AiApiConfigMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/mapper/AiModelMapper.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/mapper/AiModelMapper.java new file mode 100644 index 0000000..54259cc --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/mapper/AiModelMapper.java @@ -0,0 +1,12 @@ +package com.cool.modules.aiconfig.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.aiconfig.entity.AiModelEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI模型配置Mapper + */ +@Mapper +public interface AiModelMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/mapper/AiTemplateMapper.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/mapper/AiTemplateMapper.java new file mode 100644 index 0000000..d626e7a --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/mapper/AiTemplateMapper.java @@ -0,0 +1,12 @@ +package com.cool.modules.aiconfig.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.aiconfig.entity.AiTemplateEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI模板提示词配置Mapper + */ +@Mapper +public interface AiTemplateMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/AiApiConfigService.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/AiApiConfigService.java new file mode 100644 index 0000000..9716295 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/AiApiConfigService.java @@ -0,0 +1,24 @@ +package com.cool.modules.aiconfig.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.aiconfig.entity.AiApiConfigEntity; + +/** + * AI模型接口配置服务接口 + */ +public interface AiApiConfigService extends BaseService { + + /** + * 根据模型ID获取接口配置 + * @param modelId 模型ID + * @return 接口配置 + */ + AiApiConfigEntity getByModelId(Long modelId); + + /** + * 测试接口连通性 + * @param id 配置ID + * @return 测试结果 + */ + boolean testConnection(Long id); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/AiModelService.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/AiModelService.java new file mode 100644 index 0000000..16fede3 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/AiModelService.java @@ -0,0 +1,25 @@ +package com.cool.modules.aiconfig.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.aiconfig.entity.AiModelEntity; + +import java.util.List; + +/** + * AI模型配置服务接口 + */ +public interface AiModelService extends BaseService { + + /** + * 根据模型标识获取模型配置 + * @param modelKey 模型标识 + * @return 模型配置 + */ + AiModelEntity getByModelKey(String modelKey); + + /** + * 获取所有启用的模型列表 + * @return 启用的模型列表 + */ + List getEnabledModels(); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/AiTemplateService.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/AiTemplateService.java new file mode 100644 index 0000000..7f36460 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/AiTemplateService.java @@ -0,0 +1,33 @@ +package com.cool.modules.aiconfig.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.aiconfig.entity.AiTemplateEntity; + +import java.util.List; + +/** + * AI模板提示词配置服务接口 + */ +public interface AiTemplateService extends BaseService { + + /** + * 根据模板标识获取模板配置 + * @param templateKey 模板标识 + * @return 模板配置 + */ + AiTemplateEntity getByTemplateKey(String templateKey); + + /** + * 根据场景获取模板列表 + * @param scene 使用场景 + * @return 模板列表 + */ + List getByScene(String scene); + + /** + * 根据模型ID获取模板列表 + * @param modelId 模型ID + * @return 模板列表 + */ + List getByModelId(Long modelId); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/impl/AiApiConfigServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/impl/AiApiConfigServiceImpl.java new file mode 100644 index 0000000..0f276a6 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/impl/AiApiConfigServiceImpl.java @@ -0,0 +1,125 @@ +package com.cool.modules.aiconfig.service.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.exception.CoolPreconditions; +import com.cool.modules.aiconfig.entity.AiApiConfigEntity; +import com.cool.modules.aiconfig.mapper.AiApiConfigMapper; +import com.cool.modules.aiconfig.service.AiApiConfigService; +import com.mybatisflex.core.query.QueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * AI模型接口配置服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AiApiConfigServiceImpl extends BaseServiceImpl implements AiApiConfigService { + + @Override + public AiApiConfigEntity getByModelId(Long modelId) { + log.info("【AI接口配置】根据模型ID获取接口配置, modelId: {}", modelId); + + CoolPreconditions.check(modelId == null, "模型ID不能为空"); + + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(AiApiConfigEntity::getModelId, modelId) + .eq(AiApiConfigEntity::getStatus, 1); + + AiApiConfigEntity entity = this.getOne(queryWrapper); + + if (entity != null) { + log.info("【AI接口配置】查询成功, configId: {}, apiUrl: {}", entity.getId(), entity.getApiUrl()); + } else { + log.warn("【AI接口配置】未找到对应的接口配置, modelId: {}", modelId); + } + + return entity; + } + + @Override + public boolean testConnection(Long id) { + log.info("【AI接口配置】测试接口连通性, configId: {}", id); + + CoolPreconditions.check(id == null, "配置ID不能为空"); + + AiApiConfigEntity config = this.getById(id); + CoolPreconditions.check(config == null, "接口配置不存在"); + + try { + log.info("【AI接口配置】开始测试连接, apiUrl: {}", config.getApiUrl()); + + // 构建HTTP请求 + HttpRequest request; + if ("GET".equalsIgnoreCase(config.getRequestMethod())) { + request = HttpRequest.get(config.getApiUrl()); + } else { + request = HttpRequest.post(config.getApiUrl()); + } + + // 设置超时时间 + request.timeout(config.getTimeout() * 1000); + + // 设置请求头 + if (StrUtil.isNotEmpty(config.getRequestHeaders())) { + JSONObject headers = JSONUtil.parseObj(config.getRequestHeaders()); + for (Map.Entry entry : headers.entrySet()) { + request.header(entry.getKey(), String.valueOf(entry.getValue())); + } + } + + // 设置API密钥 + if (StrUtil.isNotEmpty(config.getApiKey()) && StrUtil.isNotEmpty(config.getApiKeyName())) { + String apiKeyValue = config.getApiKey(); + // 如果API密钥名称是Authorization且密钥不以Bearer开头,自动添加Bearer前缀 + if ("Authorization".equalsIgnoreCase(config.getApiKeyName()) + && !apiKeyValue.toLowerCase().startsWith("bearer ")) { + apiKeyValue = "Bearer " + apiKeyValue; + } + request.header(config.getApiKeyName(), apiKeyValue); + log.info("【AI接口配置】设置API密钥, keyName: {}", config.getApiKeyName()); + } + + // POST请求需要发送测试请求体 + if ("POST".equalsIgnoreCase(config.getRequestMethod())) { + // 构建一个简单的测试请求体(针对通义千问等大模型API) + String testBody = "{\"model\":\"qwen-max\",\"input\":{\"messages\":[{\"role\":\"user\",\"content\":\"测试连接\"}]},\"parameters\":{\"max_tokens\":10}}"; + request.body(testBody); + log.info("【AI接口配置】发送测试请求体: {}", testBody); + } + + // 发送请求 + HttpResponse response = request.execute(); + String responseBody = response.body(); + + log.info("【AI接口配置】测试完成, statusCode: {}, responseBody: {}", + response.getStatus(), responseBody); + + // 判断是否成功:状态码2xx或响应体不包含明显的错误信息 + boolean isSuccess = response.isOk() || + (response.getStatus() >= 200 && response.getStatus() < 500 && + StrUtil.isNotEmpty(responseBody) && !responseBody.contains("\"code\":\"InvalidApiKey\"")); + + if (isSuccess) { + log.info("【AI接口配置】接口连接测试成功"); + } else { + log.warn("【AI接口配置】接口连接测试失败, statusCode: {}, body: {}", + response.getStatus(), responseBody); + } + + return isSuccess; + } catch (Exception e) { + log.error("【AI接口配置】测试连接失败, error: {}", e.getMessage(), e); + return false; + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/impl/AiModelServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/impl/AiModelServiceImpl.java new file mode 100644 index 0000000..6728765 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/impl/AiModelServiceImpl.java @@ -0,0 +1,61 @@ +package com.cool.modules.aiconfig.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.aiconfig.entity.AiModelEntity; +import com.cool.modules.aiconfig.mapper.AiModelMapper; +import com.cool.modules.aiconfig.service.AiModelService; +import com.mybatisflex.core.query.QueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * AI模型配置服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AiModelServiceImpl extends BaseServiceImpl implements AiModelService { + + @Override + public AiModelEntity getByModelKey(String modelKey) { + log.info("【AI模型配置】根据模型标识获取模型配置, modelKey: {}", modelKey); + + if (StrUtil.isEmpty(modelKey)) { + log.warn("【AI模型配置】模型标识为空"); + return null; + } + + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(AiModelEntity::getModelKey, modelKey); + + AiModelEntity entity = this.getOne(queryWrapper); + + if (entity != null) { + log.info("【AI模型配置】查询成功, modelId: {}, modelName: {}", entity.getId(), entity.getModelName()); + } else { + log.warn("【AI模型配置】未找到对应的模型配置, modelKey: {}", modelKey); + } + + return entity; + } + + @Override + public List getEnabledModels() { + log.info("【AI模型配置】获取所有启用的模型列表"); + + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(AiModelEntity::getStatus, 1) + .orderBy(AiModelEntity::getOrderNum, true) + .orderBy(AiModelEntity::getCreateTime, false); + + List list = this.list(queryWrapper); + + log.info("【AI模型配置】查询成功, 启用模型数量: {}", list.size()); + + return list; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/impl/AiTemplateServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/impl/AiTemplateServiceImpl.java new file mode 100644 index 0000000..3142979 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/aiconfig/service/impl/AiTemplateServiceImpl.java @@ -0,0 +1,89 @@ +package com.cool.modules.aiconfig.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.aiconfig.entity.AiTemplateEntity; +import com.cool.modules.aiconfig.mapper.AiTemplateMapper; +import com.cool.modules.aiconfig.service.AiTemplateService; +import com.mybatisflex.core.query.QueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * AI模板提示词配置服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AiTemplateServiceImpl extends BaseServiceImpl implements AiTemplateService { + + @Override + public AiTemplateEntity getByTemplateKey(String templateKey) { + log.info("【AI模板配置】根据模板标识获取模板配置, templateKey: {}", templateKey); + + if (StrUtil.isEmpty(templateKey)) { + log.warn("【AI模板配置】模板标识为空"); + return null; + } + + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(AiTemplateEntity::getTemplateKey, templateKey); + + AiTemplateEntity entity = this.getOne(queryWrapper); + + if (entity != null) { + log.info("【AI模板配置】查询成功, templateId: {}, templateName: {}", entity.getId(), entity.getTemplateName()); + } else { + log.warn("【AI模板配置】未找到对应的模板配置, templateKey: {}", templateKey); + } + + return entity; + } + + @Override + public List getByScene(String scene) { + log.info("【AI模板配置】根据场景获取模板列表, scene: {}", scene); + + if (StrUtil.isEmpty(scene)) { + log.warn("【AI模板配置】场景为空"); + return List.of(); + } + + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(AiTemplateEntity::getScene, scene) + .eq(AiTemplateEntity::getStatus, 1) + .orderBy(AiTemplateEntity::getOrderNum, true) + .orderBy(AiTemplateEntity::getCreateTime, false); + + List list = this.list(queryWrapper); + + log.info("【AI模板配置】查询成功, scene: {}, 模板数量: {}", scene, list.size()); + + return list; + } + + @Override + public List getByModelId(Long modelId) { + log.info("【AI模板配置】根据模型ID获取模板列表, modelId: {}", modelId); + + if (modelId == null) { + log.warn("【AI模板配置】模型ID为空"); + return List.of(); + } + + QueryWrapper queryWrapper = QueryWrapper.create() + .eq(AiTemplateEntity::getModelId, modelId) + .eq(AiTemplateEntity::getStatus, 1) + .orderBy(AiTemplateEntity::getOrderNum, true) + .orderBy(AiTemplateEntity::getCreateTime, false); + + List list = this.list(queryWrapper); + + log.info("【AI模板配置】查询成功, modelId: {}, 模板数量: {}", modelId, list.size()); + + return list; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/AdminBaseCodingController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/AdminBaseCodingController.java new file mode 100644 index 0000000..c1cce5c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/AdminBaseCodingController.java @@ -0,0 +1,43 @@ +package com.cool.modules.base.controller.admin; + + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.annotation.TokenIgnore; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.request.R; +import com.cool.modules.base.dto.sys.CodeContentDto; +import com.cool.modules.base.service.sys.BaseCodingService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; + +/** + * ai 编码 + */ +@CoolRestController +@RequiredArgsConstructor +public class AdminBaseCodingController { + + private final BaseCodingService baseCodingService; + + @TokenIgnore + @Operation(summary = "获取模块目录结构", description = "获取模块目录结构") + @GetMapping("/getModuleTree") + public R getModuleTree() { + return R.ok(baseCodingService.getModuleTree()); + } + + @TokenIgnore + @Operation(summary = "创建代码", description = "创建代码") + @PostMapping("/createCode") + public R createCode(@RequestAttribute JSONObject requestParams) { + JSONArray codes = requestParams.get("codes", JSONArray.class); + CoolPreconditions.checkEmpty(codes); + this.baseCodingService.createCode(codes.toList(CodeContentDto.class)); + return R.ok(); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/AdminBaseCommController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/AdminBaseCommController.java new file mode 100644 index 0000000..451ab3c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/AdminBaseCommController.java @@ -0,0 +1,107 @@ +package com.cool.modules.base.controller.admin; + +import cn.hutool.core.lang.Dict; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.annotation.TokenIgnore; +import com.cool.core.eps.CoolEps; +import com.cool.core.file.FileUploadStrategyFactory; +import com.cool.core.request.R; +import com.cool.core.util.I18nUtil; +import com.cool.modules.base.entity.sys.BaseSysMenuEntity; +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 java.util.List; +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; + + @TokenIgnore + @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) { + Dict permmenu = baseSysPermsService.permmenu(adminUserId); + List list = (List) permmenu.getObj("menus"); + list.forEach(o -> o.setName(I18nUtil.getI18nMenu(o.getName()))); + return R.ok(permmenu); + } + + @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(); + } + + @TokenIgnore + @Operation(summary = "编程") + @GetMapping("/program") + public R program() { + return R.ok("Java"); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/AdminBaseOpenController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/AdminBaseOpenController.java new file mode 100644 index 0000000..ed87680 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/AdminBaseOpenController.java @@ -0,0 +1,121 @@ +package com.cool.modules.base.controller.admin; + +import static com.cool.core.plugin.consts.PluginConsts.captchaHook; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.cache.CoolCache; +import com.cool.core.enums.UserTypeEnum; +import com.cool.core.eps.CoolEps; +import com.cool.core.plugin.service.CoolPluginService; +import com.cool.core.request.R; +import com.cool.core.util.CoolPluginInvokers; +import com.cool.modules.base.dto.sys.BaseSysLoginDto; +import com.cool.modules.base.service.sys.BaseSysLoginService; +import com.cool.modules.plugin.entity.PluginInfoEntity; +import io.micrometer.common.util.StringUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * 系统开放接口,无需权限校验 + */ +@RequiredArgsConstructor +@Tag(name = "系统开放", description = "系统开放") +@CoolRestController() +public class AdminBaseOpenController { + + final private BaseSysLoginService baseSysLoginService; + final private CoolPluginService coolPluginService; + final private CoolEps coolEps; + final private CoolCache coolCache; + + @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(UserTypeEnum.ADMIN, type, width, height)); + } + + @Operation(summary = "刷新token") + @GetMapping("/refreshToken") + public R refreshToken(String refreshToken) { + return R.ok(baseSysLoginService.refreshToken(refreshToken)); + } + + @RequestMapping("/gen") + @ResponseBody + public Object genCaptcha(@RequestParam(value = "type", required = false)String type) { + if (StringUtils.isBlank(type)) { + type = "SLIDER"; + } + if ("RANDOM".equals(type)) { + int i = ThreadLocalRandom.current().nextInt(0, 4); + if (i == 0) { + type = "SLIDER"; + } else if (i == 1) { + type = "CONCAT"; + } else if (i == 2) { + type = "ROTATE"; + } else{ + type = "WORD_IMAGE_CLICK"; + } + + } + return CoolPluginInvokers.invoke("tianai", "generateCaptcha", type); + } + + @PostMapping("/check") + @ResponseBody + public Object checkCaptcha(@RequestAttribute() JSONObject requestParams) { + Object result = CoolPluginInvokers.invoke("tianai", "matching", requestParams); + Map map = BeanUtil.beanToMap(result); + if (ObjUtil.equals(map.get("code"), 200)) { + String code = ThreadLocalRandom.current().nextInt(100000, 999999) + ""; + coolCache.set("verify:img:" + requestParams.getStr("id"), code, 1800); + R r = new R(); + r.put("data", Map.of("id", requestParams.getStr("id"), + "code", code)); + r.put("code", map.get("code")); + return r; + } + return result; + } + + @Operation(summary = "验证码类型") + @GetMapping("/captchaMode") + public R captchaMode() { + PluginInfoEntity pluginInfoEntity = coolPluginService.getPluginInfoEntityByHook( + captchaHook); + if (pluginInfoEntity != null) { + return R.ok(CoolPluginInvokers.invoke(pluginInfoEntity.getKey(), "getMode")); + } + return R.ok(Map.of("mode", "common")); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysDepartmentController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysDepartmentController.java new file mode 100644 index 0000000..b969055 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysLogController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysLogController.java new file mode 100644 index 0000000..7b1ff35 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysMenuController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysMenuController.java new file mode 100644 index 0000000..5890cd4 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysMenuController.java @@ -0,0 +1,66 @@ +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.CrudOption; +import com.cool.core.request.R; +import com.cool.core.util.I18nUtil; +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) { + CrudOption transform = createOp() + .transformValue(o -> { + BaseSysMenuEntity entity = (BaseSysMenuEntity) o; + entity.setName(I18nUtil.getI18nMenu(entity.getName())); + }); + setPageOption(transform); + setListOption(transform); + setInfoOption(transform); + } + + @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/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysParamController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysParamController.java new file mode 100644 index 0000000..56f7291 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysParamController.java @@ -0,0 +1,33 @@ +package com.cool.modules.base.controller.admin.sys; + +import static com.cool.modules.base.entity.sys.table.BaseSysParamEntityTableDef.BASE_SYS_PARAM_ENTITY; + +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 jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * 系统参数配置 + */ +@Tag(name = "系统参数配置", description = "系统参数配置") +@CoolRestController(api = { "add", "delete", "update", "page", "info" }) +public class AdminBaseSysParamController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption(createOp().fieldEq(BASE_SYS_PARAM_ENTITY.DATA_TYPE) + .keyWordLikeFields(BASE_SYS_PARAM_ENTITY.NAME, BASE_SYS_PARAM_ENTITY.KEY_NAME)); + } + + @Operation(summary = "根据键返回网页的参数值") + @GetMapping("/html") + public String html(String key) { + return service.htmlByKey(key); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysRoleController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysRoleController.java new file mode 100644 index 0000000..51f0993 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysRoleController.java @@ -0,0 +1,37 @@ +package com.cool.modules.base.controller.admin.sys; + +import static com.cool.modules.base.entity.sys.table.BaseSysRoleEntityTableDef.BASE_SYS_ROLE_ENTITY; + +import cn.hutool.json.JSONArray; +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 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.getLong("userId")).or(w -> { + Object o = tokenInfo.get("roleIds"); + if (o != null) { + w.in(BASE_SYS_ROLE_ENTITY.ID.getName(), new JSONArray(o).toList(Long.class)); + } + }); + }, !isAdmin).and(BASE_SYS_ROLE_ENTITY.LABEL.ne("admin")))); + } + +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysUserController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/admin/sys/AdminBaseSysUserController.java new file mode 100644 index 0000000..aa15e18 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/controller/app/AppBaseCommController.java b/cool-admin-java/src/main/java/com/cool/modules/base/controller/app/AppBaseCommController.java new file mode 100644 index 0000000..d326b0a --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/controller/app/AppBaseCommController.java @@ -0,0 +1,71 @@ +package com.cool.modules.base.controller.app; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.annotation.TokenIgnore; +import com.cool.core.eps.CoolEps; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.file.FileUploadStrategyFactory; +import com.cool.core.request.R; +import com.cool.modules.base.service.sys.BaseSysParamService; +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.List; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +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.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +/** + * app通用接口 + */ +@RequiredArgsConstructor +@Tag(name = "应用通用", description = "应用通用") +@CoolRestController +public class AppBaseCommController { + + private final CoolEps coolEps; + + private final BaseSysParamService baseSysParamService; + + @Value("${cool.sysParam.allowKeys:[]}") + private List allowKeys; + + final private FileUploadStrategyFactory fileUploadStrategyFactory; + + @TokenIgnore + @Operation(summary = "参数配置") + @GetMapping("/param") + public R param(@RequestAttribute() JSONObject requestParams) { + String key = requestParams.get("key", String.class); + CoolPreconditions.check(!allowKeys.contains(key), "非法操作"); + return R.ok(baseSysParamService.dataByKey(key)); + } + + @TokenIgnore + @Operation(summary = "实体信息与路径", description = "系统所有的实体信息与路径,供前端自动生成代码与服务") + @GetMapping("/eps") + public R eps() { + return R.ok(coolEps.getApp()); + } + + + @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/cool-admin-java/src/main/java/com/cool/modules/base/dto/sys/BaseSysLoginDto.java b/cool-admin-java/src/main/java/com/cool/modules/base/dto/sys/BaseSysLoginDto.java new file mode 100644 index 0000000..20f3149 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/dto/sys/CodeContentDto.java b/cool-admin-java/src/main/java/com/cool/modules/base/dto/sys/CodeContentDto.java new file mode 100644 index 0000000..bcff941 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/dto/sys/CodeContentDto.java @@ -0,0 +1,9 @@ +package com.cool.modules.base.dto.sys; + +import lombok.Data; + +@Data +public class CodeContentDto { + private String path; + private String content; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysConfEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysConfEntity.java new file mode 100644 index 0000000..059035e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysConfEntity.java @@ -0,0 +1,24 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; +import org.dromara.autotable.annotation.enums.IndexTypeEnum; + +/** + * 系统配置 + */ +@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/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysDepartmentEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysDepartmentEntity.java new file mode 100644 index 0000000..406079b --- /dev/null +++ b/cool-admin-java/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.cool.core.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/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysLogEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysLogEntity.java new file mode 100644 index 0000000..765da40 --- /dev/null +++ b/cool-admin-java/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.annotation.Table; +import com.cool.core.mybatis.handler.Fastjson2TypeHandler; +import com.cool.core.annotation.ColumnDefine; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; + +@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/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysMenuEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysMenuEntity.java new file mode 100644 index 0000000..bd90aba --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysMenuEntity.java @@ -0,0 +1,52 @@ +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.cool.core.annotation.ColumnDefine; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; + +@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 = "权限", type = "text") + private String perms; + + @ColumnDefine(comment = "类型 0:目录 1:菜单 2:按钮", 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/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysParamEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysParamEntity.java new file mode 100644 index 0000000..a1dee25 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysParamEntity.java @@ -0,0 +1,29 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; + +@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") + private Integer dataType; + + @ColumnDefine(comment = "备注") + private String remark; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleDepartmentEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleDepartmentEntity.java new file mode 100644 index 0000000..6225f7b --- /dev/null +++ b/cool-admin-java/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.cool.core.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/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleEntity.java new file mode 100644 index 0000000..be0ba82 --- /dev/null +++ b/cool-admin-java/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.cool.core.mybatis.handler.Fastjson2TypeHandler; +import com.cool.core.annotation.ColumnDefine; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; +import org.dromara.autotable.annotation.enums.IndexTypeEnum; + +@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/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleMenuEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysRoleMenuEntity.java new file mode 100644 index 0000000..a563b34 --- /dev/null +++ b/cool-admin-java/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.cool.core.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/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserEntity.java new file mode 100644 index 0000000..bb148e1 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserEntity.java @@ -0,0 +1,72 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.TenantEntity; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Ignore; +import org.dromara.autotable.annotation.Index; +import org.dromara.autotable.annotation.enums.IndexTypeEnum; + +import java.util.List; + +@Getter +@Setter +@Table(value = "base_sys_user", comment = "系统用户表") +public class BaseSysUserEntity extends TenantEntity { + @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") + private Integer status; + + // 部门名称 + @Column(ignore = true) + private String departmentName; + + // 角色名称 + @Column(ignore = true) + private String roleName; + + @ColumnDefine(comment = "socketId") + private String socketId; + + + @Ignore + @Schema( description = "角色列表" ) + private List roleIdList; + + +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserRoleEntity.java b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserRoleEntity.java new file mode 100644 index 0000000..5be234c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/entity/sys/BaseSysUserRoleEntity.java @@ -0,0 +1,21 @@ +package com.cool.modules.base.entity.sys; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; + +@Getter +@Setter +@Table(value = "base_sys_user_role", comment = "系统用户角色表") +public class BaseSysUserRoleEntity extends BaseEntity { + @Index + @ColumnDefine(comment = "用户ID", type = "bigint") + private Long userId; + + @Index + @ColumnDefine(comment = "角色ID", type = "bigint") + private Long roleId; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/filter/BaseLogFilter.java b/cool-admin-java/src/main/java/com/cool/modules/base/filter/BaseLogFilter.java new file mode 100644 index 0000000..9e10474 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysConfMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysConfMapper.java new file mode 100644 index 0000000..298448c --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysDepartmentMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysDepartmentMapper.java new file mode 100644 index 0000000..cc3148e --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysLogMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysLogMapper.java new file mode 100644 index 0000000..5bd6f09 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysMenuMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysMenuMapper.java new file mode 100644 index 0000000..ab39f68 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysParamMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysParamMapper.java new file mode 100644 index 0000000..49ce1bd --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleDepartmentMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleDepartmentMapper.java new file mode 100644 index 0000000..9f3f027 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMapper.java new file mode 100644 index 0000000..6264504 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMenuMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysRoleMenuMapper.java new file mode 100644 index 0000000..4d488f0 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserMapper.java new file mode 100644 index 0000000..37f4c90 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserRoleMapper.java b/cool-admin-java/src/main/java/com/cool/modules/base/mapper/sys/BaseSysUserRoleMapper.java new file mode 100644 index 0000000..51b8105 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/security/JwtUserDetailsServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/security/JwtUserDetailsServiceImpl.java new file mode 100644 index 0000000..574dce7 --- /dev/null +++ b/cool-admin-java/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.getId(), 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/cool-admin-java/src/main/java/com/cool/modules/base/security/MySecurityMetadataSource.java b/cool-admin-java/src/main/java/com/cool/modules/base/security/MySecurityMetadataSource.java new file mode 100644 index 0000000..2f97be4 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/security/MySecurityMetadataSource.java @@ -0,0 +1,81 @@ +package com.cool.modules.base.security; + +import cn.hutool.core.util.ObjectUtil; +import com.cool.core.enums.UserTypeEnum; +import com.cool.core.util.CoolSecurityUtil; +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 { + UserTypeEnum userTypeEnum = CoolSecurityUtil.getCurrentUserType(); + if (ObjectUtil.equal(userTypeEnum, UserTypeEnum.APP)) { + // app用户不需要权限拦截 + return null; + } + 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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseCodingService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseCodingService.java new file mode 100644 index 0000000..303fb62 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseCodingService.java @@ -0,0 +1,11 @@ +package com.cool.modules.base.service.sys; + +import com.cool.modules.base.dto.sys.CodeContentDto; + +import java.util.List; + +public interface BaseCodingService { + List getModuleTree(); + + void createCode(List codes); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysConfService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysConfService.java new file mode 100644 index 0000000..3117a6f --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysDepartmentService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysDepartmentService.java new file mode 100644 index 0000000..502862d --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysLogService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysLogService.java new file mode 100644 index 0000000..4ca596d --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysLoginService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysLoginService.java new file mode 100644 index 0000000..242d8f2 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysLoginService.java @@ -0,0 +1,48 @@ +package com.cool.modules.base.service.sys; + +import com.cool.core.enums.UserTypeEnum; +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(UserTypeEnum userTypeEnum, String type, Integer width, Integer height); + + /** + * 校验验证码 + */ + void captchaCheck(String captchaId, String code); + + /** + * 登录 + * + * @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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysMenuService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysMenuService.java new file mode 100644 index 0000000..e7753d9 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysParamService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysParamService.java new file mode 100644 index 0000000..7926140 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysPermsService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysPermsService.java new file mode 100644 index 0000000..e9cd9c2 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysPermsService.java @@ -0,0 +1,168 @@ +package com.cool.modules.base.service.sys; + +import cn.hutool.core.lang.Dict; +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[] getDepartmentIdsByUserId(Long userId); + + /** + * 根据用户ID获得部门ID + * + * @param userId 角色ID数组 + * @return 部门ID数组 + */ + Long[] getDepartmentIdsByRoleIds(Long userId); + + /** + * 获得角色数组 + * + * @param userEntity 用户 + * @return 返回角色数组 + */ + Long[] getRoles(BaseSysUserEntity userEntity); + + /** + * 所有的操作权限 + * + * @return 返回所有的操作权限 + */ + String[] getAllPerms(); + + /** + * 用户的权限菜单 + * + * @param adminUserId 登录的用户 + * @return 权限菜单 + */ + Dict 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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysRoleService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysRoleService.java new file mode 100644 index 0000000..f9e4546 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysUserService.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/BaseSysUserService.java new file mode 100644 index 0000000..0a6197a --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseCodingServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseCodingServiceImpl.java new file mode 100644 index 0000000..627b35f --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseCodingServiceImpl.java @@ -0,0 +1,83 @@ +package com.cool.modules.base.service.sys.impl; + +import com.cool.core.exception.CoolPreconditions; +import com.cool.modules.base.dto.sys.CodeContentDto; +import com.cool.modules.base.service.sys.BaseCodingService; +import com.google.common.collect.Lists; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class BaseCodingServiceImpl implements BaseCodingService { + + @Value("${spring.profiles.active}") + private String env; + + // 获取模块目录结构 + public List getModuleTree() { + if (!"local".equals(env)) { + return List.of(); // 返回空列表 + } + + // 获取基础目录 + Path modulesPath = getModulesPath(); + // 获取模块文件夹 + try { + return Files.list(modulesPath) + .filter(path -> !path.getFileName().toString().equals(".DS_Store")) + .map(path -> path.getFileName().toString()) + .collect(Collectors.toList()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Path getModulesPath() { + String moduleDir = System.getProperty("user.dir"); // 可通过其他方式获取应用目录 + String packageName = BaseCodingServiceImpl.class.getPackageName(); + String modulesParentPath = packageName.split("modules")[0].replace(".", File.separator); + return Paths.get(moduleDir, "src", "main", "java", modulesParentPath, "modules"); + } + + // 创建代码文件 + public void createCode(List codes) { + if (!"local".equals(env)) { + throw new IllegalArgumentException("只能在开发环境下创建代码"); + } + Path modulesPath = getModulesPath(); + String absolutePathStr = modulesPath.toAbsolutePath().toString(); + List list = Lists.newArrayList(); + try { + for (CodeContentDto code : codes) { + // 格式化代码内容 + String formattedContent = code.getContent(); + Path filePath = Paths.get(absolutePathStr, code.getPath().replace("java/", "/")); + Path dirPath = filePath.getParent(); + // 确保目录存在 + if (!Files.exists(dirPath)) { + Files.createDirectories(dirPath); + } + // 写入文件 + try (FileWriter writer = new FileWriter(filePath.toFile())) { + formattedContent = formattedContent.replace("com.tangzc.mybatisflex.autotable.annotation.Index;", + "org.dromara.autotable.annotation.Index;"); + writer.write(formattedContent); + } + list.add(filePath.toString()); + } + } catch (Exception e) { + CoolPreconditions.alwaysThrow("生成代码失败", e); + } + log.info("代码已生成,请先编译后,手动重启服务"); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysConfServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysConfServiceImpl.java new file mode 100644 index 0000000..db130a9 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysDepartmentServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysDepartmentServiceImpl.java new file mode 100644 index 0000000..2c284cd --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysDepartmentServiceImpl.java @@ -0,0 +1,84 @@ +package com.cool.modules.base.service.sys.impl; + +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.util.CoolSecurityUtil; +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.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 BaseSysPermsService baseSysPermsService; + + @Override + public void order(List list) { + list.forEach(baseSysDepartmentEntity -> { + UpdateChain.of(BaseSysDepartmentEntity.class) + .set(BaseSysDepartmentEntity::getOrderNum, baseSysDepartmentEntity.getOrderNum()) + .set(BaseSysDepartmentEntity::getParentId, baseSysDepartmentEntity.getParentId()) + .eq(BaseSysDepartmentEntity::getId, baseSysDepartmentEntity.getId()).update(); + }); + } + + @Override + public List list(JSONObject requestParams, QueryWrapper queryWrapper) { + String username = CoolSecurityUtil.getAdminUsername(); + 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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLogServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLogServiceImpl.java new file mode 100644 index 0000000..fd4886c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLogServiceImpl.java @@ -0,0 +1,115 @@ +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.config.LogProperties; +import com.cool.core.security.IgnoredUrlsProperties; +import com.cool.core.util.CoolSecurityUtil; +import com.cool.core.util.IPUtils; +import com.cool.core.util.PathUtils; +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.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 java.util.concurrent.Executor; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 系统日志 + */ +@RequiredArgsConstructor +@Service +public class BaseSysLogServiceImpl extends BaseServiceImpl + implements BaseSysLogService { + + private final BaseSysConfService baseSysConfService; + + private final IgnoredUrlsProperties ignoredUrlsProperties; + + private final IPUtils ipUtils; + + private final LogProperties logProperties; + + private final Executor logTaskExecutor; + + @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(); + if (isIgnoreUrl(requestURI)) { + // 配置了忽略记录请求日志 + return; + } + String ipAddr = ipUtils.getIpAddr(request); + // 异步记录日志 + recordAsync(ipAddr, requestURI, requestParams); + } + + private boolean isIgnoreUrl(String requestURI) { + return PathUtils.isMatch(ignoredUrlsProperties.getLogUrls(), requestURI); + } + + public void recordAsync(String ipAddr, String requestURI, JSONObject requestParams) { + logTaskExecutor.execute(() -> { + JSONObject userInfo = CoolSecurityUtil.getAdminUserInfo(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"); + if (newJSONObject.toString().getBytes().length > logProperties.getMaxByteLength()) { + // 超过指定 + newJSONObject.clear(); + } + BaseSysLogEntity logEntity = new BaseSysLogEntity(); + logEntity.setAction(requestURI); + logEntity.setIp(ipAddr); + if (userId != null) { + logEntity.setUserId(userId); + } + logEntity.setParams(newJSONObject); + save(logEntity); + + }); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLoginServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysLoginServiceImpl.java new file mode 100644 index 0000000..635f268 --- /dev/null +++ b/cool-admin-java/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.ObjUtil; +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.enums.UserTypeEnum; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.security.jwt.JwtTokenUtil; +import com.cool.core.util.CoolSecurityUtil; +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.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 JwtTokenUtil jwtTokenUtil; + + private final BaseSysUserMapper baseSysUserMapper; + + private final BaseSysPermsService baseSysPermsService; + + @Override + public Object captcha(UserTypeEnum userTypeEnum, String type, Integer width, Integer height) { + // 1、生成验证码 2、生成对应的ID并设置在缓存中,验证码过期时间30分钟; + Map result = new HashMap<>(); + String captchaId = StrUtil.uuid(); + result.put("captchaId", captchaId); + GifCaptcha gifCaptcha = CaptchaUtil.createGifCaptcha(width, height); + if (ObjUtil.equals(userTypeEnum, UserTypeEnum.APP)) { + gifCaptcha.setGenerator(new RandomGenerator("0123456789", 4)); + } else { + gifCaptcha.setGenerator(new RandomGenerator(4)); + } + 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 void captchaCheck(String captchaId, String code) { + String key = "verify:img:" + captchaId; + String verifyCode = coolCache.get(key, + String.class); + boolean flag = StrUtil.isNotEmpty(verifyCode) + && verifyCode.equalsIgnoreCase(code); + if (!flag) { + coolCache.del(key); + CoolPreconditions.alwaysThrow("验证码不正确"); + } + } + + @Override + public Object login(BaseSysLoginDto baseSysLoginDto) { + // 1、检查验证码是否正确 2、执行登录操作 + captchaCheck(baseSysLoginDto.getCaptchaId(), baseSysLoginDto.getVerifyCode()); + UsernamePasswordAuthenticationToken upToken = + new UsernamePasswordAuthenticationToken( + baseSysLoginDto.getUsername(), baseSysLoginDto.getPassword()); + Authentication authentication = authenticationManager.authenticate(upToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + // 查询用户信息并生成token + BaseSysUserEntity baseSysUserEntity = + baseSysUserMapper.selectOneByQuery( + QueryWrapper.create() + .eq(BaseSysUserEntity::getUsername, baseSysLoginDto.getUsername())); + CoolPreconditions.check( + ObjectUtil.isEmpty(baseSysUserEntity) || baseSysUserEntity.getStatus() == 0, "用户已禁用"); + Long[] roleIds = baseSysPermsService.getRoles(baseSysUserEntity); + coolCache.del("verify:img:" + baseSysLoginDto.getCaptchaId()); + return generateToken(roleIds, baseSysUserEntity, null); + } + + @Override + public void logout(Long adminUserId, String username) { + CoolSecurityUtil.adminLogout(adminUserId, username); + } + + @Override + public Object refreshToken(String refreshToken) { + CoolPreconditions.check(!jwtTokenUtil.validateRefreshToken(refreshToken), "错误的refreshToken"); + JWT jwt = jwtTokenUtil.getTokenInfo(refreshToken); + CoolPreconditions.check(jwt == null || !(Boolean) jwt.getPayload("isRefresh"), + "错误的refreshToken"); + BaseSysUserEntity baseSysUserEntity = + baseSysUserMapper.selectOneById(Convert.toLong(jwt.getPayload("userId"))); + Long[] roleIds = baseSysPermsService.getRoles(baseSysUserEntity); + return generateToken(roleIds, baseSysUserEntity, refreshToken); + } + + private Dict generateToken(Long[] roleIds, BaseSysUserEntity baseSysUserEntity, String refreshToken) { + Dict tokenInfo = + Dict.create() + .set("userType", UserTypeEnum.ADMIN.name()) + .set("roleIds", roleIds) + .set("username", baseSysUserEntity.getUsername()) + .set("userId", baseSysUserEntity.getId()) + .set("tenantId", baseSysUserEntity.getTenantId()) + .set("passwordVersion", baseSysUserEntity.getPasswordV()); + String token = jwtTokenUtil.generateToken(tokenInfo); + if (StrUtil.isEmpty(refreshToken)) { + refreshToken = jwtTokenUtil.generateRefreshToken(tokenInfo); + } + return Dict.create() + .set("token", token) + .set("expire", jwtTokenUtil.getExpire()) + .set("refreshToken", refreshToken) + .set("refreshExpire", jwtTokenUtil.getRefreshExpire()); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysMenuServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysMenuServiceImpl.java new file mode 100644 index 0000000..0e3d930 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysMenuServiceImpl.java @@ -0,0 +1,180 @@ +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.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import com.cool.CoolApplication; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.base.ModifyEnum; +import com.cool.core.eps.CoolEps; +import com.cool.core.i18n.I18nGenerator; +import com.cool.core.util.CompilerUtils; +import com.cool.core.util.CoolSecurityUtil; +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.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 CoolEps coolEps; + + @Override + public Object list(JSONObject requestParams, QueryWrapper queryWrapper) { + List list = baseSysPermsService.getMenus(CoolSecurityUtil.getAdminUsername()); + 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); + } + } + if (ModifyEnum.ADD.equals(type) || ModifyEnum.UPDATE.equals(type)) { + SpringUtil.getBean(I18nGenerator.class).asyncGenBaseMenu(); + } + } + + @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); + SpringUtil.getBean(I18nGenerator.class).asyncGenBaseMenu(); + 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) { + coolEps.clear(); + 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"); + List javaPathList = new ArrayList<>(); + String modulesPath = PathUtils.getModulesPath(); + // 创建的模块地址 + String actModulePath = CompilerUtils.createModule(modulesPath, module); + // 创建顺序不能调整,类加载的时候需按这个顺序加载,否则类找不到 + // 创建 entity + String entityPath = CompilerUtils.createEntity(actModulePath, fileName, entity); + javaPathList.add(entityPath); + // 创建 mapper + javaPathList.add(CompilerUtils.createMapper(actModulePath, fileName, mapper)); + // 创建 service + javaPathList.add(CompilerUtils.createService(actModulePath, fileName, service)); + // 创建 serviceImpl + javaPathList.add(CompilerUtils.createServiceImpl(actModulePath, fileName, serviceImpl)); + // 创建 controller + javaPathList.add(CompilerUtils.createController(actModulePath, fileName, controller)); + // 构建TableDef + CompilerUtils.compilerEntityTableDef(actModulePath, fileName, entityPath, javaPathList); + // 重启 + CoolApplication.restart(javaPathList); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysParamServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysParamServiceImpl.java new file mode 100644 index 0000000..30607c6 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysPermsServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysPermsServiceImpl.java new file mode 100644 index 0000000..144bd84 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysPermsServiceImpl.java @@ -0,0 +1,267 @@ +package com.cool.modules.base.service.sys.impl; + +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; + +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.CoolSecurityUtil; +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.service.sys.BaseSysPermsService; +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.row.Row; +import java.util.*; +import java.util.concurrent.ExecutorService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +@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 ExecutorService cachedThreadPool; + + @Override + public Long[] loginDepartmentIds() { + String username = CoolSecurityUtil.getAdminUsername(); + 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) { + if (ObjectUtil.isEmpty(roleIds)) { + return new Long[]{}; + } + QueryWrapper queryWrapper = QueryWrapper.create(); + if (!CollUtil.toList(roleIds).contains(1L)) { + queryWrapper.in(BaseSysRoleDepartmentEntity::getRoleId, (Object) roleIds); + } + return baseSysRoleDepartmentMapper + .selectListByQuery(queryWrapper) + .stream().map(BaseSysRoleDepartmentEntity::getDepartmentId).toArray(Long[]::new); + } + + @Override + public Long[] getDepartmentIdsByRoleIds(Long userId) { + Long[] roleIds = getRoles(userId); + return getLongs(roleIds); + } + + @Override + public Long[] getDepartmentIdsByUserId(Long userId) { + return (Long[]) coolCache.get("admin:department:" + userId); + } + + @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.leftJoin(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 Dict 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)); + List batchRoleMenuList = new ArrayList<>(); + for (Long menuId : menuIdList) { + BaseSysRoleMenuEntity roleMenuEntity = new BaseSysRoleMenuEntity(); + roleMenuEntity.setRoleId(roleId); + roleMenuEntity.setMenuId(menuId); + batchRoleMenuList.add(roleMenuEntity); + } + if (ObjectUtil.isNotEmpty(batchRoleMenuList)) { + baseSysRoleMenuMapper.insertBatch(batchRoleMenuList); + } + // 更新部门权限 + baseSysRoleDepartmentMapper + .deleteByQuery(QueryWrapper.create().eq(BaseSysRoleDepartmentEntity::getRoleId, roleId)); + List batchRoleDepartmentList = new ArrayList<>(); + for (Long departmentId : departmentIds) { + BaseSysRoleDepartmentEntity roleDepartmentEntity = new BaseSysRoleDepartmentEntity(); + roleDepartmentEntity.setRoleId(roleId); + roleDepartmentEntity.setDepartmentId(departmentId); + batchRoleDepartmentList.add(roleDepartmentEntity); + } + if (ObjectUtil.isNotEmpty(batchRoleDepartmentList)) { + baseSysRoleDepartmentMapper.insertBatch(batchRoleDepartmentList); + } + cachedThreadPool.submit(() -> { + // 刷新对应角色用户的权限 + 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.adminLogout(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/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysRoleServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysRoleServiceImpl.java new file mode 100644 index 0000000..159ae97 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysRoleServiceImpl.java @@ -0,0 +1,98 @@ +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.core.util.CoolSecurityUtil; +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.service.sys.BaseSysPermsService; +import com.cool.modules.base.service.sys.BaseSysRoleService; +import com.mybatisflex.core.query.QueryWrapper; + +import java.util.Arrays; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 系统角色 + */ +@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; + + @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.getAdminUserInfo(requestParams).getLong("userId"))); + return super.add(requestParams, entity); + } + + @Override + public BaseSysRoleEntity 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); + + + roleEntity.setMenuIdList(Arrays.asList( menuIdList )); + roleEntity.setDepartmentIdList(Arrays.asList(departmentIdList )); + + } + + return roleEntity; + } + + @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 -> { + JSONObject object = CoolSecurityUtil.getAdminUserInfo(requestParams); + qw.eq(BaseSysRoleEntity::getUserId, object.get("userId")).or(w -> { + w.in(BaseSysRoleEntity::getId, + (Object) object.get("roleIds", Long[].class)); + }); + }, !CoolSecurityUtil.getAdminUsername().equals("admin"))); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysUserServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysUserServiceImpl.java new file mode 100644 index 0000000..f150bbd --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/base/service/sys/impl/BaseSysUserServiceImpl.java @@ -0,0 +1,191 @@ +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.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.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.core.util.CoolSecurityUtil; +import com.cool.core.util.DatabaseDialectUtils; +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.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; + +import java.util.Collection; +import java.util.List; + +/** + * 系统用户 + */ +@Service +@RequiredArgsConstructor +public class BaseSysUserServiceImpl extends BaseServiceImpl + implements BaseSysUserService { + + final private CoolCache coolCache; + + 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); + CoolPreconditions.checkEmpty(departmentIds); + Collection intersectionDep = Convert.toList(Long.class, departmentIds); + if (!CoolSecurityUtil.isSuperAdmin()) { + // 用户的部门权限 + Long[] permsDepartmentArr = baseSysPermsService.getDepartmentIdsByUserId(CoolSecurityUtil.getCurrentUserId()); + CoolPreconditions.returnNoData(permsDepartmentArr); + // 取交集 + intersectionDep = CollUtil.intersection(Convert.toList(Long.class, departmentIds), Convert.toList(Long.class, permsDepartmentArr)); + CoolPreconditions.returnNoData(intersectionDep); + } + + if (DatabaseDialectUtils.isPostgresql()) { + // 兼容postgresql + qw.select("base_sys_user.id","base_sys_user.create_time","base_sys_user.department_id", + "base_sys_user.email","base_sys_user.head_img","base_sys_user.name","base_sys_user.nick_name", + "base_sys_user.phone","base_sys_user.remark","base_sys_user.status", + "base_sys_user.update_time","base_sys_user.username", + "string_agg(base_sys_role.name, ', ') AS roleName", + "base_sys_department.name AS departmentName" + ); + } else { + qw.select(BASE_SYS_USER_ENTITY.ALL_COLUMNS, + groupConcat(BASE_SYS_ROLE_ENTITY.NAME).as("roleName"), + BASE_SYS_DEPARTMENT_ENTITY.NAME.as("departmentName") + ); + } + + qw.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.ROLE_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.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(intersectionDep, ArrayUtil.isNotEmpty(intersectionDep))); + if (DatabaseDialectUtils.isPostgresql()) { + // 兼容postgresql + qw.groupBy("base_sys_user.id","base_sys_user.create_time","base_sys_user.department_id", + "base_sys_user.email","base_sys_user.head_img","base_sys_user.name","base_sys_user.nick_name", + "base_sys_user.phone","base_sys_user.remark","base_sys_user.status", + "base_sys_user.update_time","base_sys_user.username", + "base_sys_department.name"); + } else { + qw.groupBy(BASE_SYS_USER_ENTITY.ID); + } + return mapper.paginate(page, qw); + } + + @Override + public void personUpdate(Long userId, Dict body) { + BaseSysUserEntity userEntity = getById(userId); + CoolPreconditions.checkEmpty(userEntity, "用户不存在"); + userEntity.setNickName(body.getStr("nickName")); + userEntity.setHeadImg(body.getStr("headImg")); + // 修改密码 + 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.adminLogout(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 BaseSysUserEntity info(Long id) { + BaseSysUserEntity userEntity = getById(id); + Long[] roleIdList = baseSysPermsService.getRoles(id); + BaseSysDepartmentEntity departmentEntity = baseSysDepartmentMapper.selectOneById( + userEntity.getDepartmentId()); + userEntity.setPassword(null); + + + userEntity.setRoleIdList(List.of(roleIdList)); + userEntity.setDepartmentName(departmentEntity != null ? departmentEntity.getName() : userEntity.getDepartmentName() ); + + return userEntity; + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/controller/admin/AdminReportController.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/controller/admin/AdminReportController.java new file mode 100644 index 0000000..e469ed2 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/controller/admin/AdminReportController.java @@ -0,0 +1,359 @@ +package com.cool.modules.dailyreport.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.dailyreport.entity.DailyReportEntity; +import com.cool.modules.dailyreport.service.DailyReportService; +import com.cool.modules.weeklyreport.entity.WeeklyReportEntity; +import com.cool.modules.weeklyreport.service.WeeklyReportService; +import com.cool.modules.monthlyreport.entity.MonthlyReportEntity; +import com.cool.modules.monthlyreport.service.MonthlyReportService; +import com.cool.modules.dailyreport.service.DailyReportAiService; +import com.cool.modules.base.service.sys.BaseSysParamService; +import com.cool.modules.user.entity.UserInfoEntity; +import com.cool.modules.user.mapper.UserInfoMapper; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.cool.modules.dailyreport.entity.table.DailyReportEntityTableDef.DAILY_REPORT_ENTITY; +import static com.cool.modules.user.entity.table.UserInfoEntityTableDef.USER_INFO_ENTITY; + +/** + * 管理端日报Controller + */ +@Slf4j +@Tag(name = "管理端日报", description = "管理端日报查看和统计") +@CoolRestController(api = {"page", "list", "info"}) +@RequiredArgsConstructor +public class AdminReportController extends BaseController { + + private final UserInfoMapper userInfoMapper; + private final DailyReportAiService dailyReportAiService; + private final BaseSysParamService baseSysParamService; + private final WeeklyReportService weeklyReportService; + private final MonthlyReportService monthlyReportService; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + log.info("【管理端日报Controller】接收到请求参数: {}", requestParams); + + // 构建查询条件 + QueryWrapper qw = QueryWrapper.create() + .orderBy(DAILY_REPORT_ENTITY.REPORT_DATE, false) // 按日期倒序 + .orderBy(DAILY_REPORT_ENTITY.CREATE_TIME, false); // 按创建时间倒序 + + // 部门ID筛选(需要关联user_info表) + if (requestParams.containsKey("deptId") && requestParams.get("deptId") != null) { + Long deptId = requestParams.getLong("deptId"); + log.info("【管理端日报Controller】添加deptId筛选条件: {}", deptId); + // 需要join user_info表来筛选部门 + qw.and(DAILY_REPORT_ENTITY.USER_ID.in( + QueryWrapper.create() + .select(USER_INFO_ENTITY.ID) + .from(USER_INFO_ENTITY) + .where(USER_INFO_ENTITY.DINGTALK_DEPT_ID.eq(deptId)) + )); + } + + // 开始日期筛选 + if (requestParams.containsKey("startDate") && requestParams.get("startDate") != null) { + String startDate = requestParams.getStr("startDate"); + log.info("【管理端日报Controller】添加startDate筛选条件: {}", startDate); + qw.and(DAILY_REPORT_ENTITY.REPORT_DATE.ge(LocalDate.parse(startDate))); + } + + // 结束日期筛选 + if (requestParams.containsKey("endDate") && requestParams.get("endDate") != null) { + String endDate = requestParams.getStr("endDate"); + log.info("【管理端日报Controller】添加endDate筛选条件: {}", endDate); + qw.and(DAILY_REPORT_ENTITY.REPORT_DATE.le(LocalDate.parse(endDate))); + } + + setPageOption(createOp() + .keyWordLikeFields(DAILY_REPORT_ENTITY.USER_EDITED_CONTENT) // 关键词模糊搜索 + .fieldEq(DAILY_REPORT_ENTITY.STATUS) // 状态等值筛选 + .queryWrapper(qw) + ); + } + + @Operation(summary = "获取组织AI建议", description = "基于时间范围聚合内容生成组织层面的建议") + @GetMapping("/insight") + public R getOrgInsight( + @Parameter(description = "范围:week 或 month,默认 week") @RequestParam(required = false, defaultValue = "week") String scope, + @Parameter(description = "开始日期 yyyy-MM-dd(周一或任意日)") @RequestParam(required = false) String startDate, + @Parameter(description = "结束日期 yyyy-MM-dd(可选,未传按scope推导)") @RequestParam(required = false) String endDate + ) { + try { + LocalDate start; + LocalDate end; + if (startDate == null || startDate.isEmpty()) { + start = LocalDate.now(); + } else { + start = LocalDate.parse(startDate); + } + if ("month".equalsIgnoreCase(scope)) { + // 月范围:按当月起止 + LocalDate first = start.withDayOfMonth(1); + LocalDate last = (endDate != null && !endDate.isEmpty()) ? LocalDate.parse(endDate) : first.plusMonths(1).minusDays(1); + start = first; + end = last; + } else { + // 周范围:起始至起始+6 + end = (endDate != null && !endDate.isEmpty()) ? LocalDate.parse(endDate) : start.plusDays(6); + } + + StringBuilder sb = new StringBuilder(); + + // 1) 月度优先:基于月报聚合内容 + if ("month".equalsIgnoreCase(scope)) { + String monthKey = start.getYear() + "-" + String.format("%02d", start.getMonthValue()); + QueryWrapper mq = QueryWrapper.create() + .eq(MonthlyReportEntity::getMonth, monthKey) + .eq(MonthlyReportEntity::getStatus, 1) + .orderBy(MonthlyReportEntity::getSubmitTime, true); + + var monthlyList = monthlyReportService.list(mq); + for (MonthlyReportEntity r : monthlyList) { + String content = r.getUserEditedContent(); + if (content == null || content.isBlank()) content = r.getAiFormattedContent(); + if (content == null || content.isBlank()) content = r.getOriginalText(); + if (content != null && !content.isBlank()) { + sb.append("- ") + .append(r.getMonth()).append(": ") + .append(content) + .append("\n"); + } + } + + // 若按 month 字段未命中,则退化为 submitTime 区间匹配 + if (sb.length() == 0) { + QueryWrapper mq2 = QueryWrapper.create() + .ge(MonthlyReportEntity::getSubmitTime, start.atStartOfDay()) + .le(MonthlyReportEntity::getSubmitTime, end.atTime(23, 59, 59)) + .eq(MonthlyReportEntity::getStatus, 1) + .orderBy(MonthlyReportEntity::getSubmitTime, true); + + var monthlyList2 = monthlyReportService.list(mq2); + for (MonthlyReportEntity r : monthlyList2) { + String content = r.getUserEditedContent(); + if (content == null || content.isBlank()) content = r.getAiFormattedContent(); + if (content == null || content.isBlank()) content = r.getOriginalText(); + if (content != null && !content.isBlank()) { + sb.append("- ") + .append(r.getMonth()).append(": ") + .append(content) + .append("\n"); + } + } + } + } + + // 2) 若为空:聚合日报 + if (sb.length() == 0) { + QueryWrapper qw = QueryWrapper.create() + .ge(DailyReportEntity::getReportDate, start) + .le(DailyReportEntity::getReportDate, end) + .eq(DailyReportEntity::getStatus, 1) + .orderBy(DailyReportEntity::getReportDate, true); + + List reports = this.service.list(qw); + for (DailyReportEntity r : reports) { + String content = r.getUserEditedContent(); + if (content == null || content.isBlank()) { + content = r.getAiFormattedContent(); + } + if (content == null || content.isBlank()) { + content = r.getOriginalText(); + } + if (content != null && !content.isBlank()) { + sb.append("- ") + .append(r.getReportDate()).append(": ") + .append(content) + .append("\n"); + } + } + } + + // 3) 若仍为空,且为月度范围:回退基于周报的聚合 + if (sb.length() == 0 && "month".equalsIgnoreCase(scope)) { + QueryWrapper wq = QueryWrapper.create() + .ge(WeeklyReportEntity::getWeekStartDate, start) + .le(WeeklyReportEntity::getWeekEndDate, end) + .eq(WeeklyReportEntity::getStatus, 1) + .orderBy(WeeklyReportEntity::getWeekStartDate, true); + + List weeklyList = weeklyReportService.list(wq); + for (WeeklyReportEntity r : weeklyList) { + String content = r.getUserEditedContent(); + if (content == null || content.isBlank()) content = r.getAiFormattedContent(); + if (content == null || content.isBlank()) content = r.getOriginalText(); + if (content != null && !content.isBlank()) { + sb.append("- ") + .append(r.getWeekStartDate()).append(" ~ ").append(r.getWeekEndDate()).append(": ") + .append(content) + .append("\n"); + } + } + } + + // 若聚合内容为空,直接返回更友好的提示 + if (sb.length() == 0) { + String msg = "month".equalsIgnoreCase(scope) + ? "未查询到该月份的月报数据,请核实后再尝试" + : "未查询到该周期的数据,请核实后再尝试"; + return R.error(msg); + } + + String defaultKey = "month".equalsIgnoreCase(scope) ? "org_insight_monthly" : "org_insight_weekly"; + String paramKey = "month".equalsIgnoreCase(scope) ? "ai.insight.template.monthly" : "ai.insight.template.weekly"; + String configuredKey = baseSysParamService.dataByKey(paramKey); + String templateKey = (configuredKey != null && !configuredKey.isBlank()) ? configuredKey : defaultKey; + String insight = dailyReportAiService.generateAdvice(templateKey, sb.toString()); + + JSONObject result = new JSONObject(); + result.set("scope", scope); + result.set("startDate", start.toString()); + result.set("endDate", end.toString()); + result.set("insightText", insight); + return R.ok(result); + } catch (Exception e) { + log.error("【管理端日报Controller】获取组织AI建议失败", e); + return R.error("获取组织AI建议失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取统计信息", description = "获取日报提交统计信息") + @GetMapping("/statistics") + public R getStatistics( + @Parameter(description = "开始日期 yyyy-MM-dd") @RequestParam(required = false) String startDate, + @Parameter(description = "结束日期 yyyy-MM-dd") @RequestParam(required = false) String endDate + ) { + log.info("【管理端日报Controller】获取统计信息, startDate: {}, endDate: {}", startDate, endDate); + + try { + // 解析日期 + LocalDate start = startDate != null ? LocalDate.parse(startDate) : LocalDate.now().minusDays(7); + LocalDate end = endDate != null ? LocalDate.parse(endDate) : LocalDate.now(); + + // 获取统计信息 + Map statistics = this.service.getStatistics(start, end); + + log.info("【管理端日报Controller】统计信息获取成功: {}", statistics); + return R.ok(statistics); + + } catch (Exception e) { + log.error("【管理端日报Controller】获取统计信息失败", e); + return R.error("获取统计信息失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取未提交人员列表", description = "获取指定日期未提交日报的人员列表") + @GetMapping("/unsubmitted") + public R getUnsubmittedUsers( + @Parameter(description = "日报日期 yyyy-MM-dd") @RequestParam(required = false) String reportDate + ) { + log.info("【管理端日报Controller】获取未提交人员列表, reportDate: {}", reportDate); + + try { + // 解析日期(默认今天) + LocalDate date = reportDate != null ? LocalDate.parse(reportDate) : LocalDate.now(); + + // 获取未提交的用户ID列表 + List unsubmittedUserIds = this.service.getUnsubmittedUserIds(date); + + // 查询用户详细信息 + List users = new ArrayList<>(); + if (!unsubmittedUserIds.isEmpty()) { + QueryWrapper qw = QueryWrapper.create() + .where(USER_INFO_ENTITY.ID.in(unsubmittedUserIds)); + users = userInfoMapper.selectListByQuery(qw); + } + + log.info("【管理端日报Controller】未提交人员数量: {}", users.size()); + + Map result = new HashMap<>(); + result.put("date", date); + result.put("unsubmittedCount", users.size()); + result.put("users", users); + + return R.ok(result); + + } catch (Exception e) { + log.error("【管理端日报Controller】获取未提交人员列表失败", e); + return R.error("获取未提交人员列表失败: " + e.getMessage()); + } + } + + @Operation(summary = "按部门统计提交情况", description = "统计各部门的日报提交率") + @GetMapping("/departmentStatistics") + public R getDepartmentStatistics( + @Parameter(description = "开始日期 yyyy-MM-dd") @RequestParam(required = false) String startDate, + @Parameter(description = "结束日期 yyyy-MM-dd") @RequestParam(required = false) String endDate + ) { + log.info("【管理端日报Controller】按部门统计提交情况, startDate: {}, endDate: {}", startDate, endDate); + + try { + // 解析日期 + LocalDate start = startDate != null ? LocalDate.parse(startDate) : LocalDate.now().minusDays(7); + LocalDate end = endDate != null ? LocalDate.parse(endDate) : LocalDate.now(); + + // TODO: 实现按部门统计的详细逻辑 + // 这里需要查询每个部门的人数、提交数量等信息 + // 由于涉及复杂的统计查询,建议在Service层实现 + + Map result = new HashMap<>(); + result.put("message", "功能开发中,待实现"); + result.put("startDate", start); + result.put("endDate", end); + + return R.ok(result); + + } catch (Exception e) { + log.error("【管理端日报Controller】按部门统计失败", e); + return R.error("按部门统计失败: " + e.getMessage()); + } + } + + @Operation(summary = "导出日报", description = "导出日报数据为Excel格式") + @GetMapping("/export") + public R export( + @Parameter(description = "用户ID") @RequestParam(required = false) Long userId, + @Parameter(description = "开始日期 yyyy-MM-dd") @RequestParam(required = false) String startDate, + @Parameter(description = "结束日期 yyyy-MM-dd") @RequestParam(required = false) String endDate, + @Parameter(description = "状态") @RequestParam(required = false) Integer status + ) { + log.info("【管理端日报Controller】导出日报, userId: {}, startDate: {}, endDate: {}, status: {}", + userId, startDate, endDate, status); + + try { + // TODO: 实现导出功能 + // 1. 根据条件查询日报数据 + // 2. 使用 EasyExcel 或 POI 生成 Excel 文件 + // 3. 返回文件下载响应 + + Map result = new HashMap<>(); + result.put("message", "功能开发中,待实现"); + + return R.ok(result); + + } catch (Exception e) { + log.error("【管理端日报Controller】导出日报失败", e); + return R.error("导出失败: " + e.getMessage()); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/controller/app/AppDailyReportController.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/controller/app/AppDailyReportController.java new file mode 100644 index 0000000..667a388 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/controller/app/AppDailyReportController.java @@ -0,0 +1,288 @@ +package com.cool.modules.dailyreport.controller.app; + +import cn.hutool.json.JSONObject; +import com.cool.core.request.R; +import com.cool.modules.dailyreport.entity.DailyReportEntity; +import com.cool.modules.dailyreport.service.DailyReportAiService; +import com.cool.modules.dailyreport.service.DailyReportService; +import com.cool.modules.dailyreport.service.SpeechToTextService; +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 lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户端日报Controller + */ +@Slf4j +@Tag(name = "用户端日报", description = "用户端日报提交和查询") +@RestController +@RequestMapping("/app/dailyreport/report") +@RequiredArgsConstructor +public class AppDailyReportController { + + private final DailyReportService dailyReportService; + private final SpeechToTextService speechToTextService; + private final DailyReportAiService dailyReportAiService; + + @Operation(summary = "上传语音并转文字", description = "上传语音文件,后端调用语音识别服务转换为文字") + @PostMapping("/uploadVoice") + public R uploadVoice( + @Parameter(description = "语音文件") @RequestParam("file") MultipartFile file, + @Parameter(description = "音频格式") @RequestParam(value = "format", defaultValue = "wav") String format + ) { + log.info("【用户端日报Controller】上传语音, 文件名: {}, 大小: {} bytes, 格式: {}", + file.getOriginalFilename(), file.getSize(), format); + + try { + // 验证文件 + if (file.isEmpty()) { + log.error("【用户端日报Controller】上传失败:文件为空"); + return R.error("请选择要上传的语音文件"); + } + + // 获取音频数据 + byte[] audioData = file.getBytes(); + + // 调用语音识别服务 + String recognizedText = speechToTextService.convertToText(audioData, format); + + log.info("【用户端日报Controller】语音识别成功, 结果长度: {} 字符", recognizedText.length()); + + JSONObject result = new JSONObject(); + result.set("text", recognizedText); + result.set("length", recognizedText.length()); + + return R.ok(result); + + } catch (Exception e) { + log.error("【用户端日报Controller】语音识别失败", e); + return R.error("语音识别失败: " + e.getMessage()); + } + } + + @Operation(summary = "AI格式化日报内容", description = "将用户输入的原始内容通过AI格式化为规范的日报格式") + @PostMapping("/aiFormat") + public R aiFormat(@RequestBody JSONObject data) { + log.info("【用户端日报Controller】AI格式化, 原文长度: {} 字符, 日期: {}", + data.getStr("originalText") != null ? data.getStr("originalText").length() : 0, + data.getStr("reportDate")); + + try { + String originalText = data.getStr("originalText"); + String templateKey = data.getStr("templateKey", "daily_report_format"); // 默认使用 daily_report_format 模板 + String reportDate = data.getStr("reportDate"); // 获取日报日期 + + // 验证输入 + if (originalText == null || originalText.trim().isEmpty()) { + log.error("【用户端日报Controller】AI格式化失败:输入内容为空"); + return R.error("输入内容不能为空"); + } + + // 调用AI格式化服务(带日期参数) + String formattedContent = dailyReportAiService.formatDailyReport(originalText, templateKey, reportDate); + + log.info("【用户端日报Controller】AI格式化成功, 结果长度: {} 字符", formattedContent.length()); + + JSONObject result = new JSONObject(); + result.set("formattedContent", formattedContent); + result.set("length", formattedContent.length()); + + return R.ok(result); + + } catch (Exception e) { + log.error("【用户端日报Controller】AI格式化失败", e); + return R.error("AI格式化失败: " + e.getMessage()); + } + } + + @Operation(summary = "保存草稿", description = "保存日报草稿,不更改状态为已提交") + @PostMapping("/saveDraft") + public R saveDraft(@RequestBody JSONObject data) { + Long userId = data.getLong("userId"); + log.info("【用户端日报Controller】保存草稿, userId: {}", userId); + + try { + // 构建日报实体 + DailyReportEntity entity = buildEntityFromRequest(data); + entity.setStatus(0); // 草稿状态 + + // 保存草稿 + DailyReportEntity saved = dailyReportService.saveDraft(entity); + + log.info("【用户端日报Controller】草稿保存成功, id: {}", saved.getId()); + return R.ok(saved); + + } catch (Exception e) { + log.error("【用户端日报Controller】保存草稿失败", e); + return R.error("保存草稿失败: " + e.getMessage()); + } + } + + @Operation(summary = "提交日报", description = "提交日报,标记状态为已提交") + @PostMapping("/submit") + public R submit(@RequestBody JSONObject data) { + Long userId = data.getLong("userId"); + log.info("【用户端日报Controller】提交日报, userId: {}", userId); + + try { + // 构建日报实体 + DailyReportEntity entity = buildEntityFromRequest(data); + + // 提交日报 + DailyReportEntity submitted = dailyReportService.submitReport(entity); + + log.info("【用户端日报Controller】日报提交成功, id: {}, submitTime: {}", + submitted.getId(), submitted.getSubmitTime()); + return R.ok(submitted); + + } catch (Exception e) { + log.error("【用户端日报Controller】提交日报失败", e); + return R.error("提交日报失败: " + e.getMessage()); + } + } + + @Operation(summary = "更新日报", description = "更新已有的日报内容") + @PostMapping("/update") + public R update(@RequestBody JSONObject data) { + Long userId = data.getLong("userId"); + Long reportId = data.getLong("id"); + log.info("【用户端日报Controller】更新日报, userId: {}, reportId: {}", userId, reportId); + + try { + // 构建日报实体 + DailyReportEntity entity = buildEntityFromRequest(data); + entity.setId(reportId); + + // 更新日报 + DailyReportEntity updated = dailyReportService.updateReport(entity); + + log.info("【用户端日报Controller】日报更新成功, id: {}", updated.getId()); + return R.ok(updated); + + } catch (Exception e) { + log.error("【用户端日报Controller】更新日报失败", e); + return R.error("更新日报失败: " + e.getMessage()); + } + } + + @Operation(summary = "查询我的日报列表", description = "查询当前用户的日报列表,支持时间范围和状态筛选") + @GetMapping("/myReports") + public R getMyReports( + @Parameter(description = "用户ID") @RequestParam Long userId, + @Parameter(description = "开始日期 yyyy-MM-dd") @RequestParam(required = false) String startDate, + @Parameter(description = "结束日期 yyyy-MM-dd") @RequestParam(required = false) String endDate, + @Parameter(description = "状态 0-草稿 1-已提交") @RequestParam(required = false) Integer status, + @Parameter(description = "页码") @RequestParam(required = false, defaultValue = "1") Integer page, + @Parameter(description = "每页大小") @RequestParam(required = false, defaultValue = "20") Integer size + ) { + log.info("【用户端日报Controller】查询我的日报列表, userId: {}, startDate: {}, endDate: {}, status: {}, page: {}, size: {}", + userId, startDate, endDate, status, page, size); + + try { + // 解析日期 + LocalDate start = startDate != null ? LocalDate.parse(startDate) : null; + LocalDate end = endDate != null ? LocalDate.parse(endDate) : null; + + // 查询日报列表(暂时不分页,返回全部数据) + List reports = dailyReportService.getUserReports(userId, start, end, status); + + log.info("【用户端日报Controller】查询成功, 数量: {}", reports.size()); + return R.ok(reports); + + } catch (Exception e) { + log.error("【用户端日报Controller】查询我的日报列表失败", e); + return R.error("查询失败: " + e.getMessage()); + } + } + + @Operation(summary = "查询日报详情", description = "根据日报ID查询详细信息") + @GetMapping("/detail") + public R getDetail( + @Parameter(description = "日报ID") @RequestParam Long id, + @Parameter(description = "用户ID") @RequestParam Long userId + ) { + log.info("【用户端日报Controller】查询日报详情, id: {}, userId: {}", id, userId); + + try { + // 查询日报 + DailyReportEntity report = dailyReportService.getById(id); + + if (report == null) { + log.warn("【用户端日报Controller】日报不存在, id: {}", id); + return R.error("日报不存在"); + } + + // 验证权限(只能查看自己的日报) + if (!report.getUserId().equals(userId)) { + log.error("【用户端日报Controller】无权限查看, userId: {}, ownerId: {}", userId, report.getUserId()); + return R.error("无权限查看此日报"); + } + + log.info("【用户端日报Controller】查询成功"); + return R.ok(report); + + } catch (Exception e) { + log.error("【用户端日报Controller】查询日报详情失败", e); + return R.error("查询失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取今日日报", description = "获取当前用户今天的日报(如果存在)") + @GetMapping("/todayReport") + public R getTodayReport(@Parameter(description = "用户ID") @RequestParam Long userId) { + log.info("【用户端日报Controller】获取今日日报, userId: {}", userId); + + try { + LocalDate today = LocalDate.now(); + + // 查询今日日报 + DailyReportEntity report = dailyReportService.getByUserIdAndDate(userId, today); + + if (report != null) { + log.info("【用户端日报Controller】今日日报存在, id: {}, status: {}", report.getId(), report.getStatus()); + return R.ok(report); + } else { + log.info("【用户端日报Controller】今日暂无日报"); + return R.ok(null); + } + + } catch (Exception e) { + log.error("【用户端日报Controller】获取今日日报失败", e); + return R.error("查询失败: " + e.getMessage()); + } + } + + /** + * 从请求数据构建日报实体 + * + * @param data 请求数据 + * @return 日报实体 + */ + private DailyReportEntity buildEntityFromRequest(JSONObject data) { + DailyReportEntity entity = new DailyReportEntity(); + + entity.setUserId(data.getLong("userId")); + + // 日报日期(默认今天) + String reportDateStr = data.getStr("reportDate"); + LocalDate reportDate = reportDateStr != null ? LocalDate.parse(reportDateStr) : LocalDate.now(); + entity.setReportDate(reportDate); + + entity.setOriginalText(data.getStr("originalText")); + entity.setAiFormattedContent(data.getStr("aiFormattedContent")); + entity.setUserEditedContent(data.getStr("userEditedContent")); + entity.setAiModelId(data.getLong("aiModelId")); + entity.setAiTemplateId(data.getLong("aiTemplateId")); + entity.setInputType(data.getInt("inputType", 0)); // 默认文字输入 + + return entity; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/entity/DailyReportEntity.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/entity/DailyReportEntity.java new file mode 100644 index 0000000..907bd60 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/entity/DailyReportEntity.java @@ -0,0 +1,73 @@ +package com.cool.modules.dailyreport.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 日报实体 + * 记录员工每日工作报告,包含原始输入、AI格式化和用户最终编辑的内容 + */ +@Getter +@Setter +@Table(value = "daily_report", comment = "日报表") +public class DailyReportEntity extends BaseEntity { + + @ColumnDefine(comment = "用户ID(关联user_info表)", notNull = true) + private Long userId; + + @ColumnDefine(comment = "日报日期", notNull = true) + private LocalDate reportDate; + + @ColumnDefine(comment = "原始输入内容(语音转文字或直接文字输入)", type = "TEXT") + private String originalText; + + @ColumnDefine(comment = "AI格式化后的内容", type = "TEXT") + private String aiFormattedContent; + + @ColumnDefine(comment = "用户最终编辑的内容(实际展示内容)", type = "TEXT") + private String userEditedContent; + + @ColumnDefine(comment = "使用的AI模型ID(关联ai_model表)") + private Long aiModelId; + + @ColumnDefine(comment = "使用的AI模板ID(关联ai_template表)") + private Long aiTemplateId; + + @ColumnDefine(comment = "状态 0-草稿 1-已提交", defaultValue = "0") + private Integer status; + + @ColumnDefine(comment = "提交时间") + private LocalDateTime submitTime; + + @ColumnDefine(comment = "输入方式 0-文字输入 1-语音输入", defaultValue = "0") + private Integer inputType; + + /** + * 获取状态文本(用于前端显示) + */ + @JsonProperty("statusText") + public String getStatusText() { + if (status == null) { + return "未知"; + } + return status == 0 ? "草稿" : "已提交"; + } + + /** + * 获取输入方式文本(用于前端显示) + */ + @JsonProperty("inputTypeText") + public String getInputTypeText() { + if (inputType == null) { + return "未知"; + } + return inputType == 0 ? "文字输入" : "语音输入"; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/mapper/DailyReportMapper.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/mapper/DailyReportMapper.java new file mode 100644 index 0000000..a4fa8d6 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/mapper/DailyReportMapper.java @@ -0,0 +1,12 @@ +package com.cool.modules.dailyreport.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.dailyreport.entity.DailyReportEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 日报 Mapper + */ +@Mapper +public interface DailyReportMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/DailyReportAiService.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/DailyReportAiService.java new file mode 100644 index 0000000..22256be --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/DailyReportAiService.java @@ -0,0 +1,53 @@ +package com.cool.modules.dailyreport.service; + +/** + * 日报AI格式化服务接口 + */ +public interface DailyReportAiService { + + /** + * 使用AI格式化日报内容 + * + * @param originalText 用户原始输入内容 + * @param templateKey 模板标识(如 "daily_report") + * @return 格式化后的日报内容 + * @throws RuntimeException AI格式化失败时抛出异常 + */ + String formatDailyReport(String originalText, String templateKey); + + /** + * 使用AI格式化日报内容(带日期) + * + * @param originalText 用户原始输入内容 + * @param templateKey 模板标识(如 "daily_report") + * @param reportDate 日报日期(格式:yyyy-MM-dd) + * @return 格式化后的日报内容 + * @throws RuntimeException AI格式化失败时抛出异常 + */ + String formatDailyReport(String originalText, String templateKey, String reportDate); + + /** + * 使用默认模板格式化日报内容 + * + * @param originalText 用户原始输入内容 + * @return 格式化后的日报内容 + */ + String formatDailyReportWithDefaultTemplate(String originalText); + + /** + * 检查AI服务是否可用 + * + * @return true-可用,false-不可用 + */ + boolean isAiServiceAvailable(); + + /** + * 基于模板与聚合内容生成建议文本(组织或个人)。 + * 约定模板中可使用 {content} / {{content}} 作为聚合内容占位符。 + * + * @param templateKey 模板标识(如 org_insight_weekly / user_advice_weekly 等) + * @param content 聚合后的输入内容 + * @return 建议文本 + */ + String generateAdvice(String templateKey, String content); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/DailyReportService.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/DailyReportService.java new file mode 100644 index 0000000..cb2fda7 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/DailyReportService.java @@ -0,0 +1,75 @@ +package com.cool.modules.dailyreport.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.dailyreport.entity.DailyReportEntity; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +/** + * 日报服务接口 + */ +public interface DailyReportService extends BaseService { + + /** + * 保存草稿 + * + * @param entity 日报实体 + * @return 保存结果 + */ + DailyReportEntity saveDraft(DailyReportEntity entity); + + /** + * 提交日报 + * + * @param entity 日报实体 + * @return 提交结果 + */ + DailyReportEntity submitReport(DailyReportEntity entity); + + /** + * 更新日报 + * + * @param entity 日报实体 + * @return 更新结果 + */ + DailyReportEntity updateReport(DailyReportEntity entity); + + /** + * 获取用户指定日期的日报 + * + * @param userId 用户ID + * @param reportDate 日报日期 + * @return 日报实体,不存在则返回null + */ + DailyReportEntity getByUserIdAndDate(Long userId, LocalDate reportDate); + + /** + * 查询用户的日报列表 + * + * @param userId 用户ID + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param status 状态(null表示全部) + * @return 日报列表 + */ + List getUserReports(Long userId, LocalDate startDate, LocalDate endDate, Integer status); + + /** + * 获取统计信息 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 统计信息 + */ + Map getStatistics(LocalDate startDate, LocalDate endDate); + + /** + * 获取未提交人员列表 + * + * @param reportDate 日报日期 + * @return 未提交人员ID列表 + */ + List getUnsubmittedUserIds(LocalDate reportDate); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/SpeechToTextService.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/SpeechToTextService.java new file mode 100644 index 0000000..e1550ca --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/SpeechToTextService.java @@ -0,0 +1,26 @@ +package com.cool.modules.dailyreport.service; + +/** + * 语音转文字服务接口 + */ +public interface SpeechToTextService { + + /** + * 将音频文件转换为文字 + * + * @param audioData 音频数据(字节数组) + * @param format 音频格式(如 wav, mp3, opus) + * @return 识别后的文字内容 + * @throws RuntimeException 识别失败时抛出异常 + */ + String convertToText(byte[] audioData, String format); + + /** + * 验证音频文件是否符合要求 + * + * @param audioData 音频数据 + * @param format 音频格式 + * @throws RuntimeException 验证失败时抛出异常 + */ + void validateAudio(byte[] audioData, String format); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/impl/DailyReportAiServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/impl/DailyReportAiServiceImpl.java new file mode 100644 index 0000000..3cddcbf --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/impl/DailyReportAiServiceImpl.java @@ -0,0 +1,333 @@ +package com.cool.modules.dailyreport.service.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.modules.aiconfig.entity.AiApiConfigEntity; +import com.cool.modules.aiconfig.entity.AiModelEntity; +import com.cool.modules.aiconfig.entity.AiTemplateEntity; +import com.cool.modules.aiconfig.service.AiApiConfigService; +import com.cool.modules.aiconfig.service.AiModelService; +import com.cool.modules.aiconfig.service.AiTemplateService; +import com.cool.modules.dailyreport.service.DailyReportAiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * 日报AI格式化服务实现 + * 支持调用通义千问等大模型API进行日报内容格式化 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DailyReportAiServiceImpl implements DailyReportAiService { + + private final AiTemplateService aiTemplateService; + private final AiModelService aiModelService; + private final AiApiConfigService aiApiConfigService; + + // 默认日报模板标识 + private static final String DEFAULT_TEMPLATE_KEY = "daily_report_format"; + + @Override + public String formatDailyReport(String originalText, String templateKey) { + return formatDailyReport(originalText, templateKey, null); + } + + @Override + public String formatDailyReport(String originalText, String templateKey, String reportDate) { + log.info("【日报AI格式化】开始格式化, templateKey: {}, reportDate: {}, 原文长度: {} 字符", + templateKey, reportDate, originalText != null ? originalText.length() : 0); + + try { + // 验证输入 + if (originalText == null || originalText.trim().isEmpty()) { + log.error("【日报AI格式化】输入内容为空"); + throw new RuntimeException("输入内容不能为空"); + } + + // 查询模板 + AiTemplateEntity template = aiTemplateService.getByTemplateKey(templateKey); + if (template == null) { + log.error("【日报AI格式化】模板不存在: {}", templateKey); + throw new RuntimeException("日报模板未配置,请联系管理员"); + } + + log.info("【日报AI格式化】找到模板: {}, modelId: {}", template.getTemplateName(), template.getModelId()); + + // 查询AI模型配置 + AiModelEntity model = null; + if (template.getModelId() != null) { + model = aiModelService.getById(template.getModelId()); + } + + if (model == null) { + log.warn("【日报AI格式化】模板未关联AI模型,使用默认模型"); + // 尝试获取第一个可用模型 + model = aiModelService.getEnabledModels().stream().findFirst().orElse(null); + } + + if (model == null) { + log.error("【日报AI格式化】没有可用的AI模型"); + throw new RuntimeException("AI模型未配置,请联系管理员"); + } + + log.info("【日报AI格式化】使用AI模型: {}", model.getModelName()); + + // 构建完整提示词(包含日期) + String fullPrompt = buildPrompt(template.getPromptContent(), originalText, reportDate); + + // 调用AI模型生成内容 + String formattedContent = callAiModel(model, fullPrompt); + + log.info("【日报AI格式化】格式化成功, 结果长度: {} 字符", formattedContent.length()); + return formattedContent; + + } catch (Exception e) { + log.error("【日报AI格式化】格式化失败", e); + throw new RuntimeException("AI格式化失败: " + e.getMessage()); + } + } + + @Override + public String formatDailyReportWithDefaultTemplate(String originalText) { + log.info("【日报AI格式化】使用默认模板格式化"); + return formatDailyReport(originalText, DEFAULT_TEMPLATE_KEY); + } + + @Override + public boolean isAiServiceAvailable() { + try { + // 检查是否有可用的模板和模型 + AiTemplateEntity template = aiTemplateService.getByTemplateKey(DEFAULT_TEMPLATE_KEY); + if (template == null) { + log.warn("【日报AI格式化】默认模板不存在"); + return false; + } + + java.util.List models = aiModelService.getEnabledModels(); + if (models == null || models.isEmpty()) { + log.warn("【日报AI格式化】没有启用的AI模型"); + return false; + } + + return true; + + } catch (Exception e) { + log.error("【日报AI格式化】检查AI服务可用性失败", e); + return false; + } + } + + @Override + public String generateAdvice(String templateKey, String content) { + log.info("【AI建议】开始生成建议, templateKey: {}, 聚合内容长度: {}", templateKey, + content != null ? content.length() : 0); + try { + if (content == null || content.trim().isEmpty()) { + log.error("【AI建议】输入内容为空"); + throw new RuntimeException("输入内容不能为空"); + } + + AiTemplateEntity template = aiTemplateService.getByTemplateKey(templateKey); + if (template == null) { + log.error("【AI建议】模板不存在: {}", templateKey); + throw new RuntimeException("AI建议模板未配置,请联系管理员: " + templateKey); + } + + AiModelEntity model = null; + if (template.getModelId() != null) { + model = aiModelService.getById(template.getModelId()); + } + if (model == null) { + model = aiModelService.getEnabledModels().stream().findFirst().orElse(null); + } + if (model == null) { + log.error("【AI建议】没有可用的AI模型"); + throw new RuntimeException("AI模型未配置,请联系管理员"); + } + + String prompt = buildAdvicePrompt(template.getPromptContent(), content); + String result = callAiModel(model, prompt); + log.info("【AI建议】生成成功, 长度: {}", result != null ? result.length() : 0); + return result; + } catch (Exception e) { + log.error("【AI建议】生成失败", e); + throw new RuntimeException("AI建议生成失败: " + e.getMessage()); + } + } + + private String buildAdvicePrompt(String templateContent, String content) { + if (templateContent == null) { + templateContent = ""; + } + String prompt = templateContent + .replace("{content}", content == null ? "" : content) + .replace("{{content}}", content == null ? "" : content); + return prompt; + } + + /** + * 构建完整提示词 + * + * @param templateContent 模板内容 + * @param originalText 用户原始文字 + * @param reportDate 日报日期(可选) + * @return 完整提示词 + */ + private String buildPrompt(String templateContent, String originalText, String reportDate) { + log.info("【日报AI格式化】构建提示词, reportDate: {}", reportDate); + + // 支持多种占位符格式 + String prompt = templateContent + .replace("{original_text}", originalText) + .replace("{{original_text}}", originalText) + .replace("{content}", originalText) + .replace("{{content}}", originalText); + + // 如果提供了日期,替换日期占位符 + if (reportDate != null && !reportDate.isEmpty()) { + prompt = prompt + .replace("{date}", reportDate) + .replace("{{date}}", reportDate) + .replace("{report_date}", reportDate) + .replace("{{report_date}}", reportDate); + } else { + // 如果没有提供日期,使用当前日期 + String today = java.time.LocalDate.now().toString(); + prompt = prompt + .replace("{date}", today) + .replace("{{date}}", today) + .replace("{report_date}", today) + .replace("{{report_date}}", today); + } + + log.info("【日报AI格式化】提示词构建完成, 长度: {} 字符", prompt.length()); + return prompt; + } + + /** + * 调用AI模型生成内容 + * + * @param model AI模型配置 + * @param prompt 提示词 + * @return AI生成的内容 + */ + private String callAiModel(AiModelEntity model, String prompt) { + log.info("【日报AI格式化】调用AI模型, model: {}, modelKey: {}", model.getModelName(), model.getModelKey()); + + try { + // 获取API配置 + AiApiConfigEntity apiConfig = aiApiConfigService.getByModelId(model.getId()); + if (apiConfig == null) { + log.error("【日报AI格式化】模型未配置API接口, modelId: {}", model.getId()); + throw new RuntimeException("AI模型接口未配置,请联系管理员"); + } + + log.info("【日报AI格式化】使用API配置, apiUrl: {}", apiConfig.getApiUrl()); + + // 构建请求体(通义千问格式) + JSONObject requestBody = new JSONObject(); + requestBody.set("model", model.getModelKey()); + + JSONObject input = new JSONObject(); + JSONArray messages = new JSONArray(); + JSONObject message = new JSONObject(); + message.set("role", "user"); + message.set("content", prompt); + messages.add(message); + input.set("messages", messages); + requestBody.set("input", input); + + // 设置参数 + JSONObject parameters = new JSONObject(); + parameters.set("max_tokens", 2000); + requestBody.set("parameters", parameters); + + log.info("【日报AI格式化】请求体: {}", requestBody.toString()); + + // 构建HTTP请求 + HttpRequest request = HttpRequest.post(apiConfig.getApiUrl()) + .timeout(apiConfig.getTimeout() * 1000) + .header("Content-Type", "application/json"); + + // 设置请求头 + if (StrUtil.isNotEmpty(apiConfig.getRequestHeaders())) { + JSONObject headers = JSONUtil.parseObj(apiConfig.getRequestHeaders()); + for (Map.Entry entry : headers.entrySet()) { + request.header(entry.getKey(), String.valueOf(entry.getValue())); + } + } + + // 设置API密钥 + if (StrUtil.isNotEmpty(apiConfig.getApiKey()) && StrUtil.isNotEmpty(apiConfig.getApiKeyName())) { + String apiKeyValue = apiConfig.getApiKey(); + // 如果API密钥名称是Authorization且密钥不以Bearer开头,自动添加Bearer前缀 + if ("Authorization".equalsIgnoreCase(apiConfig.getApiKeyName()) + && !apiKeyValue.toLowerCase().startsWith("bearer ")) { + apiKeyValue = "Bearer " + apiKeyValue; + } + request.header(apiConfig.getApiKeyName(), apiKeyValue); + } + + // 发送请求(适当放宽超时时间) + request.body(requestBody.toString()).timeout(120000); + HttpResponse response = request.execute(); + String responseBody = response.body(); + + log.info("【日报AI格式化】API响应, statusCode: {}, body: {}", + response.getStatus(), responseBody); + + // 检查响应状态 + if (!response.isOk()) { + log.error("【日报AI格式化】API调用失败, statusCode: {}, body: {}", + response.getStatus(), responseBody); + throw new RuntimeException("AI API调用失败,状态码: " + response.getStatus()); + } + + // 解析响应 + JSONObject result = JSONUtil.parseObj(responseBody); + + // 通义千问响应格式:output.choices[0].message.content + String content = result.getByPath("output.text", String.class); + if (StrUtil.isEmpty(content)) { + // 尝试另一种路径 + content = result.getByPath("output.choices[0].message.content", String.class); + } + + if (StrUtil.isEmpty(content)) { + log.error("【日报AI格式化】响应中未找到生成内容, response: {}", responseBody); + throw new RuntimeException("AI生成内容为空"); + } + + log.info("【日报AI格式化】AI生成成功, 内容长度: {} 字符", content.length()); + return content; + + } catch (Exception e) { + log.error("【日报AI格式化】调用AI API失败", e); + + // 降级策略:返回格式化的提示信息 + log.warn("【日报AI格式化】使用降级策略,返回原文"); + return "## AI格式化服务暂时不可用\n\n原始内容:\n\n" + + extractOriginalText(prompt); + } + } + + /** + * 从提示词中提取原始文本 + */ + private String extractOriginalText(String prompt) { + // 尝试提取 {original_text} 后的内容 + if (prompt.contains("用户输入:")) { + int start = prompt.indexOf("用户输入:") + 5; + return prompt.substring(start).trim(); + } + return prompt; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/impl/DailyReportServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/impl/DailyReportServiceImpl.java new file mode 100644 index 0000000..334d303 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/impl/DailyReportServiceImpl.java @@ -0,0 +1,287 @@ +package com.cool.modules.dailyreport.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.dailyreport.entity.DailyReportEntity; +import com.cool.modules.dailyreport.mapper.DailyReportMapper; +import com.cool.modules.dailyreport.service.DailyReportService; +import com.cool.modules.user.service.UserInfoService; +import com.mybatisflex.core.query.QueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 日报服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DailyReportServiceImpl + extends BaseServiceImpl + implements DailyReportService { + + private final UserInfoService userInfoService; + + @Override + @Transactional(rollbackFor = Exception.class) + public DailyReportEntity saveDraft(DailyReportEntity entity) { + log.info("【日报服务】保存草稿, userId: {}, reportDate: {}", entity.getUserId(), entity.getReportDate()); + + try { + // 检查是否已存在该用户当天的日报 + DailyReportEntity existing = getByUserIdAndDate(entity.getUserId(), entity.getReportDate()); + + if (existing != null) { + // 更新已有日报 + log.info("【日报服务】更新已有草稿, id: {}", existing.getId()); + entity.setId(existing.getId()); + entity.setStatus(0); // 草稿状态 + entity.setSubmitTime(null); // 清空提交时间 + this.updateById(entity); + log.info("【日报服务】草稿更新成功, id: {}", entity.getId()); + } else { + // 新增日报 + entity.setStatus(0); // 草稿状态 + entity.setSubmitTime(null); + this.save(entity); + log.info("【日报服务】草稿保存成功, id: {}", entity.getId()); + } + + return entity; + + } catch (Exception e) { + log.error("【日报服务】保存草稿失败", e); + throw new RuntimeException("保存草稿失败: " + e.getMessage()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public DailyReportEntity submitReport(DailyReportEntity entity) { + log.info("【日报服务】提交日报, userId: {}, reportDate: {}", entity.getUserId(), entity.getReportDate()); + + try { + // 验证内容不能为空 + if (entity.getUserEditedContent() == null || entity.getUserEditedContent().trim().isEmpty()) { + log.error("【日报服务】提交失败:日报内容为空"); + throw new RuntimeException("日报内容不能为空"); + } + + // 检查是否已存在该用户当天的日报 + DailyReportEntity existing = getByUserIdAndDate(entity.getUserId(), entity.getReportDate()); + + if (existing != null) { + // 更新已有日报 + log.info("【日报服务】更新已有日报并提交, id: {}", existing.getId()); + entity.setId(existing.getId()); + } + + // 设置提交状态和时间 + entity.setStatus(1); // 已提交 + entity.setSubmitTime(LocalDateTime.now()); + + if (entity.getId() != null) { + this.updateById(entity); + } else { + this.save(entity); + } + + log.info("【日报服务】日报提交成功, id: {}, submitTime: {}", entity.getId(), entity.getSubmitTime()); + return entity; + + } catch (Exception e) { + log.error("【日报服务】提交日报失败", e); + throw new RuntimeException("提交日报失败: " + e.getMessage()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public DailyReportEntity updateReport(DailyReportEntity entity) { + log.info("【日报服务】更新日报, id: {}, userId: {}", entity.getId(), entity.getUserId()); + + try { + if (entity.getId() == null) { + log.error("【日报服务】更新失败:日报ID为空"); + throw new RuntimeException("日报ID不能为空"); + } + + // 验证日报是否存在 + DailyReportEntity existing = this.getById(entity.getId()); + if (existing == null) { + log.error("【日报服务】更新失败:日报不存在, id: {}", entity.getId()); + throw new RuntimeException("日报不存在"); + } + + // 验证用户权限(只能更新自己的日报) + if (!existing.getUserId().equals(entity.getUserId())) { + log.error("【日报服务】更新失败:无权限, userId: {}, ownerId: {}", entity.getUserId(), existing.getUserId()); + throw new RuntimeException("无权限更新此日报"); + } + + // 更新日报 + this.updateById(entity); + log.info("【日报服务】日报更新成功, id: {}", entity.getId()); + + return entity; + + } catch (Exception e) { + log.error("【日报服务】更新日报失败", e); + throw new RuntimeException("更新日报失败: " + e.getMessage()); + } + } + + @Override + public DailyReportEntity getByUserIdAndDate(Long userId, LocalDate reportDate) { + log.info("【日报服务】查询用户日报, userId: {}, reportDate: {}", userId, reportDate); + + try { + QueryWrapper qw = QueryWrapper.create() + .eq(DailyReportEntity::getUserId, userId) + .eq(DailyReportEntity::getReportDate, reportDate); + + DailyReportEntity entity = this.getOne(qw); + + if (entity != null) { + log.info("【日报服务】查询成功, id: {}, status: {}", entity.getId(), entity.getStatus()); + } else { + log.info("【日报服务】未找到该日期的日报"); + } + + return entity; + + } catch (Exception e) { + log.error("【日报服务】查询用户日报失败", e); + throw new RuntimeException("查询用户日报失败: " + e.getMessage()); + } + } + + @Override + public List getUserReports(Long userId, LocalDate startDate, LocalDate endDate, Integer status) { + log.info("【日报服务】查询用户日报列表, userId: {}, startDate: {}, endDate: {}, status: {}", + userId, startDate, endDate, status); + + try { + QueryWrapper qw = QueryWrapper.create() + .eq(DailyReportEntity::getUserId, userId); + + // 添加可选条件 + if (startDate != null) { + qw.ge(DailyReportEntity::getReportDate, startDate); + } + if (endDate != null) { + qw.le(DailyReportEntity::getReportDate, endDate); + } + if (status != null) { + qw.eq(DailyReportEntity::getStatus, status); + } + + qw.orderBy(DailyReportEntity::getReportDate, false); // 按日期倒序 + + List list = this.list(qw); + log.info("【日报服务】查询成功, 数量: {}", list.size()); + + return list; + + } catch (Exception e) { + log.error("【日报服务】查询用户日报列表失败", e); + throw new RuntimeException("查询用户日报列表失败: " + e.getMessage()); + } + } + + @Override + public Map getStatistics(LocalDate startDate, LocalDate endDate) { + log.info("【日报服务】获取统计信息, startDate: {}, endDate: {}", startDate, endDate); + + try { + Map result = new HashMap<>(); + + // 查询指定时间范围内的日报 + QueryWrapper qw = QueryWrapper.create(); + if (startDate != null) { + qw.ge(DailyReportEntity::getReportDate, startDate); + } + if (endDate != null) { + qw.le(DailyReportEntity::getReportDate, endDate); + } + + List reports = this.list(qw); + + // 统计已提交数量 + long submittedCount = reports.stream() + .filter(r -> r.getStatus() == 1) + .count(); + + // 统计草稿数量 + long draftCount = reports.stream() + .filter(r -> r.getStatus() == 0) + .count(); + + // 统计总用户数 + long totalUserCount = userInfoService.count(); + + // 计算提交率(假设每天每人应提交一篇日报) + long daysBetween = endDate != null && startDate != null + ? java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) + 1 + : 1; + long expectedSubmissions = totalUserCount * daysBetween; + double submissionRate = expectedSubmissions > 0 + ? (double) submittedCount / expectedSubmissions * 100 + : 0; + + result.put("totalReports", reports.size()); + result.put("submittedCount", submittedCount); + result.put("draftCount", draftCount); + result.put("totalUserCount", totalUserCount); + result.put("expectedSubmissions", expectedSubmissions); + result.put("submissionRate", String.format("%.2f%%", submissionRate)); + + log.info("【日报服务】统计完成: {}", result); + return result; + + } catch (Exception e) { + log.error("【日报服务】获取统计信息失败", e); + throw new RuntimeException("获取统计信息失败: " + e.getMessage()); + } + } + + @Override + public List getUnsubmittedUserIds(LocalDate reportDate) { + log.info("【日报服务】获取未提交人员列表, reportDate: {}", reportDate); + + try { + // 查询所有用户ID + List allUserIds = userInfoService.list() + .stream() + .map(user -> user.getId()) + .toList(); + + // 查询已提交的用户ID + QueryWrapper qw = QueryWrapper.create() + .eq(DailyReportEntity::getReportDate, reportDate) + .eq(DailyReportEntity::getStatus, 1); // 只统计已提交的 + + List submittedUserIds = this.list(qw) + .stream() + .map(DailyReportEntity::getUserId) + .toList(); + + // 计算未提交的用户ID + List unsubmittedUserIds = allUserIds.stream() + .filter(userId -> !submittedUserIds.contains(userId)) + .toList(); + + log.info("【日报服务】未提交人员数量: {}", unsubmittedUserIds.size()); + return unsubmittedUserIds; + + } catch (Exception e) { + log.error("【日报服务】获取未提交人员列表失败", e); + throw new RuntimeException("获取未提交人员列表失败: " + e.getMessage()); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/impl/SpeechToTextServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/impl/SpeechToTextServiceImpl.java new file mode 100644 index 0000000..7e50049 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dailyreport/service/impl/SpeechToTextServiceImpl.java @@ -0,0 +1,134 @@ +package com.cool.modules.dailyreport.service.impl; + +import com.cool.modules.dailyreport.service.SpeechToTextService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 语音转文字服务实现(模拟实现) + * TODO: 后续接入真实的阿里云语音识别SDK + */ +@Slf4j +@Service +public class SpeechToTextServiceImpl implements SpeechToTextService { + + // 支持的音频格式 + private static final String[] SUPPORTED_FORMATS = {"wav", "mp3", "opus", "pcm"}; + + // 音频大小限制(2MB) + private static final long MAX_AUDIO_SIZE = 2 * 1024 * 1024; + + @Override + public String convertToText(byte[] audioData, String format) { + log.info("【语音识别】开始识别, 音频大小: {} bytes, 格式: {}", audioData.length, format); + + try { + // 验证音频 + validateAudio(audioData, format); + + // TODO: 实际集成阿里云语音识别API + // 当前为模拟实现,返回测试文本 + String result = simulateRecognition(audioData); + + log.info("【语音识别】识别成功, 结果长度: {} 字符", result.length()); + return result; + + } catch (Exception e) { + log.error("【语音识别】识别失败", e); + throw new RuntimeException("语音识别失败: " + e.getMessage()); + } + } + + @Override + public void validateAudio(byte[] audioData, String format) { + log.info("【语音识别】验证音频, 大小: {} bytes, 格式: {}", audioData.length, format); + + // 验证音频数据不为空 + if (audioData == null || audioData.length == 0) { + log.error("【语音识别】验证失败:音频数据为空"); + throw new RuntimeException("音频数据不能为空"); + } + + // 验证音频大小 + if (audioData.length > MAX_AUDIO_SIZE) { + log.error("【语音识别】验证失败:音频过大, size: {}", audioData.length); + throw new RuntimeException("音频文件不能超过2MB"); + } + + // 验证音频格式 + boolean formatSupported = false; + for (String supportedFormat : SUPPORTED_FORMATS) { + if (supportedFormat.equalsIgnoreCase(format)) { + formatSupported = true; + break; + } + } + + if (!formatSupported) { + log.error("【语音识别】验证失败:不支持的格式: {}", format); + throw new RuntimeException("不支持的音频格式,仅支持: wav, mp3, opus, pcm"); + } + + log.info("【语音识别】音频验证通过"); + } + + /** + * 模拟语音识别(用于测试) + * TODO: 替换为真实的阿里云API调用 + * + * @param audioData 音频数据 + * @return 模拟的识别结果 + */ + private String simulateRecognition(byte[] audioData) { + log.info("【语音识别】使用模拟识别(开发阶段)"); + + // 返回模拟的识别结果 + return "今天完成了用户日报模块的开发工作,包括实体类创建、服务层实现和API接口开发。" + + "遇到的问题是语音识别API还未正式接入,目前使用模拟数据进行测试。" + + "明天计划完成前端页面开发和功能测试,并接入真实的语音识别服务。"; + } + + /* + * TODO: 真实的阿里云语音识别实现示例 + * + * 需要添加依赖: + * + * com.aliyun + * aliyun-java-sdk-core + * 4.6.0 + * + * + * 实现代码: + * + * @Value("${aliyun.speech.accessKeyId}") + * private String accessKeyId; + * + * @Value("${aliyun.speech.accessKeySecret}") + * private String accessKeySecret; + * + * @Value("${aliyun.speech.appKey}") + * private String appKey; + * + * private String callAliyunApi(byte[] audioData, String format) { + * // 1. 创建客户端 + * DefaultProfile profile = DefaultProfile.getProfile("cn-shanghai", accessKeyId, accessKeySecret); + * IAcsClient client = new DefaultAcsClient(profile); + * + * // 2. 构建请求 + * CommonRequest request = new CommonRequest(); + * request.setSysMethod(MethodType.POST); + * request.setSysDomain("nls-gateway.cn-shanghai.aliyuncs.com"); + * request.setSysVersion("2018-08-17"); + * request.setSysAction("RecognizeSpeech"); + * request.putQueryParameter("appkey", appKey); + * request.putQueryParameter("format", format); + * request.putQueryParameter("sample_rate", "16000"); + * request.putBodyParameter("audio_data", Base64.getEncoder().encodeToString(audioData)); + * + * // 3. 发送请求并解析结果 + * CommonResponse response = client.getCommonResponse(request); + * JSONObject result = JSONUtil.parseObj(response.getData()); + * return result.getStr("result"); + * } + */ +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dict/controller/admin/AdminDictInfoController.java b/cool-admin-java/src/main/java/com/cool/modules/dict/controller/admin/AdminDictInfoController.java new file mode 100644 index 0000000..5fd766e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dict/controller/admin/AdminDictInfoController.java @@ -0,0 +1,57 @@ +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.annotation.TokenIgnore; +import com.cool.core.base.BaseController; +import com.cool.core.request.CrudOption; +import com.cool.core.request.R; +import com.cool.core.util.I18nUtil; +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 jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.GetMapping; +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 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)) + .transformValue(o -> { + DictInfoEntity entity = (DictInfoEntity) o; + entity.setName(I18nUtil.getI18nDictInfo(entity.getName())); + })); + CrudOption transform = createOp().transformValue(o -> { + DictInfoEntity entity = (DictInfoEntity) o; + entity.setName(I18nUtil.getI18nDictInfo(entity.getName())); + }); + setPageOption(transform); + setInfoOption(transform); + } + + @Operation(summary = "获得字典数据", description = "获得字典数据信息") + @PostMapping("/data") + public R data(@RequestBody Dict body) { + return R.ok(this.service.data(body.get("types", null))); + } + + @TokenIgnore + @GetMapping("/types") + @Operation(summary = "获得字典数据", description = "获得字典数据信息") + public R types() { + return R.ok(this.service.types()); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dict/controller/admin/AdminDictTypeController.java b/cool-admin-java/src/main/java/com/cool/modules/dict/controller/admin/AdminDictTypeController.java new file mode 100644 index 0000000..09072f9 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dict/controller/admin/AdminDictTypeController.java @@ -0,0 +1,36 @@ +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.core.request.CrudOption; +import com.cool.core.util.I18nUtil; +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).transformValue(o -> { + DictTypeEntity entity = (DictTypeEntity) o; + entity.setName(I18nUtil.getI18nDictType(entity.getName())); + })); + CrudOption transform = createOp().transformValue(o -> { + DictTypeEntity entity = (DictTypeEntity) o; + entity.setName(I18nUtil.getI18nDictType(entity.getName())); + }); + setPageOption(transform); + setInfoOption(transform); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dict/controller/app/AppDictInfoController.java b/cool-admin-java/src/main/java/com/cool/modules/dict/controller/app/AppDictInfoController.java new file mode 100644 index 0000000..bad4ba8 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/dict/entity/DictInfoEntity.java b/cool-admin-java/src/main/java/com/cool/modules/dict/entity/DictInfoEntity.java new file mode 100644 index 0000000..e4a96c3 --- /dev/null +++ b/cool-admin-java/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.cool.core.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/cool-admin-java/src/main/java/com/cool/modules/dict/entity/DictTypeEntity.java b/cool-admin-java/src/main/java/com/cool/modules/dict/entity/DictTypeEntity.java new file mode 100644 index 0000000..c3854d2 --- /dev/null +++ b/cool-admin-java/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.cool.core.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/cool-admin-java/src/main/java/com/cool/modules/dict/mapper/DictInfoMapper.java b/cool-admin-java/src/main/java/com/cool/modules/dict/mapper/DictInfoMapper.java new file mode 100644 index 0000000..6c79be5 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/dict/mapper/DictTypeMapper.java b/cool-admin-java/src/main/java/com/cool/modules/dict/mapper/DictTypeMapper.java new file mode 100644 index 0000000..1c01714 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/dict/service/DictInfoService.java b/cool-admin-java/src/main/java/com/cool/modules/dict/service/DictInfoService.java new file mode 100644 index 0000000..2dfec7e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dict/service/DictInfoService.java @@ -0,0 +1,21 @@ +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); + + Object types(); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dict/service/DictTypeService.java b/cool-admin-java/src/main/java/com/cool/modules/dict/service/DictTypeService.java new file mode 100644 index 0000000..0c17f82 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/dict/service/impl/DictInfoServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/dict/service/impl/DictInfoServiceImpl.java new file mode 100644 index 0000000..51e6506 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dict/service/impl/DictInfoServiceImpl.java @@ -0,0 +1,134 @@ +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.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.base.ModifyEnum; +import com.cool.core.i18n.I18nGenerator; +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 Object types() { + List infos = this.list(); + if (ObjUtil.isEmpty(infos)) { + return infos; + } + List datas = new ArrayList<>(); + infos.stream().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); + }); + return datas; + } + + @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); + } + } + } + @Override + public void modifyAfter(JSONObject requestParams, DictInfoEntity entity, ModifyEnum type) { + if (ModifyEnum.ADD.equals(type) || ModifyEnum.UPDATE.equals(type)) { + SpringUtil.getBean(I18nGenerator.class).asyncGenBaseDictInfo(); + } + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/dict/service/impl/DictTypeServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/dict/service/impl/DictTypeServiceImpl.java new file mode 100644 index 0000000..6373cbb --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dict/service/impl/DictTypeServiceImpl.java @@ -0,0 +1,50 @@ +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.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.base.ModifyEnum; +import com.cool.core.i18n.I18nGenerator; +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; + } + + @Override + public void modifyBefore(JSONObject requestParams, DictTypeEntity t, ModifyEnum type) { + if (ModifyEnum.ADD.equals(type) || ModifyEnum.UPDATE.equals(type)) { + SpringUtil.getBean(I18nGenerator.class).asyncGenBaseDictType(); + } + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/controller/admin/AdminDingtalkConfigController.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/controller/admin/AdminDingtalkConfigController.java new file mode 100644 index 0000000..0c3ff2c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/controller/admin/AdminDingtalkConfigController.java @@ -0,0 +1,84 @@ +package com.cool.modules.dingtalk.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.dingtalk.entity.DingtalkConfigEntity; +import com.cool.modules.dingtalk.service.DingtalkConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * 钉钉配置管理Controller + */ +@Slf4j +@Tag(name = "钉钉配置", description = "钉钉配置管理") +@CoolRestController(api = {"add", "update", "info"}) +public class AdminDingtalkConfigController extends BaseController { + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + // 钉钉配置为单例,无需分页查询 + } + + @Operation(summary = "获取钉钉配置", description = "获取当前钉钉配置(单例)") + @GetMapping("/config") + public R getConfig() { + log.info("【钉钉配置Controller】获取钉钉配置"); + + DingtalkConfigEntity config = this.service.getConfig(); + + if (config != null) { + log.info("【钉钉配置Controller】配置获取成功, ID: {}", config.getId()); + return R.ok(config); + } else { + log.warn("【钉钉配置Controller】配置不存在"); + return R.ok(null); + } + } + + @Operation(summary = "验证配置", description = "验证钉钉配置有效性(测试AccessToken获取)") + @PostMapping("/validate") + public R validateConfig(@RequestBody JSONObject data) { + log.info("【钉钉配置Controller】验证配置, data: {}", data); + + String appKey = data.getStr("appKey"); + String appSecret = data.getStr("appSecret"); + + if (appKey == null || appKey.isEmpty() || appSecret == null || appSecret.isEmpty()) { + log.warn("【钉钉配置Controller】AppKey或AppSecret为空"); + return R.error("AppKey和AppSecret不能为空"); + } + + boolean isValid = this.service.validateConfig(appKey, appSecret); + + if (isValid) { + log.info("【钉钉配置Controller】配置验证成功"); + return R.ok("配置验证成功,AccessToken获取正常"); + } else { + log.error("【钉钉配置Controller】配置验证失败"); + return R.error("配置验证失败,请检查AppKey和AppSecret是否正确"); + } + } + + @Operation(summary = "保存或更新配置", description = "保存或更新钉钉配置(单例)") + @PostMapping("/saveOrUpdate") + public R saveOrUpdateConfig(@RequestBody DingtalkConfigEntity entity) { + log.info("【钉钉配置Controller】保存或更新配置, entity: {}", entity); + + try { + DingtalkConfigEntity savedEntity = this.service.saveOrUpdateConfig(entity); + log.info("【钉钉配置Controller】配置保存成功, ID: {}", savedEntity.getId()); + return R.ok(savedEntity); + } catch (Exception e) { + log.error("【钉钉配置Controller】配置保存失败", e); + return R.error("配置保存失败: " + e.getMessage()); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/controller/admin/AdminDingtalkDepartmentController.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/controller/admin/AdminDingtalkDepartmentController.java new file mode 100644 index 0000000..4259453 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/controller/admin/AdminDingtalkDepartmentController.java @@ -0,0 +1,125 @@ +package com.cool.modules.dingtalk.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.dingtalk.entity.DingtalkDepartmentEntity; +import com.cool.modules.dingtalk.mapper.DingtalkDepartmentMapper; +import com.cool.modules.dingtalk.service.DingtalkDepartmentService; +import com.cool.modules.user.entity.UserInfoEntity; +import com.cool.modules.user.mapper.UserInfoMapper; +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 lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.cool.modules.dingtalk.entity.table.DingtalkDepartmentEntityTableDef.DINGTALK_DEPARTMENT_ENTITY; +import static com.cool.modules.user.entity.table.UserInfoEntityTableDef.USER_INFO_ENTITY; + +/** + * 钉钉部门管理Controller + */ +@Slf4j +@Tag(name = "钉钉部门", description = "钉钉部门管理") +@CoolRestController(api = {"page", "list"}) +@RequiredArgsConstructor +public class AdminDingtalkDepartmentController extends BaseController { + + private final DingtalkDepartmentMapper departmentMapper; + private final UserInfoMapper userInfoMapper; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption(createOp() + .keyWordLikeFields(DINGTALK_DEPARTMENT_ENTITY.NAME) + .queryWrapper(QueryWrapper.create().orderBy(DINGTALK_DEPARTMENT_ENTITY.DEPT_ORDER, true)) + ); + } + + @Operation(summary = "获取部门树", description = "获取部门树结构") + @GetMapping("/tree") + public R getDepartmentTree() { + log.info("【钉钉部门Controller】获取部门树"); + + try { + // 获取所有部门 + List allDepts = departmentMapper.selectAll(); + + // 构建树结构 + List> tree = buildTree(allDepts, 1L); + + log.info("【钉钉部门Controller】部门树获取成功, 根节点数量: {}", tree.size()); + return R.ok(tree); + + } catch (Exception e) { + log.error("【钉钉部门Controller】获取部门树失败", e); + return R.error("获取部门树失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取部门员工", description = "根据钉钉部门ID获取该部门的员工列表") + @GetMapping("/users") + public R getDepartmentUsers(@RequestParam Long deptId) { + log.info("【钉钉部门Controller】获取部门员工, deptId: {}", deptId); + + try { + // 查询该部门下的所有员工 + QueryWrapper qw = QueryWrapper.create() + .where(USER_INFO_ENTITY.DINGTALK_DEPT_ID.eq(deptId)) + .orderBy(USER_INFO_ENTITY.CREATE_TIME, false); + + List users = userInfoMapper.selectListByQuery(qw); + + log.info("【钉钉部门Controller】部门员工获取成功, 数量: {}", users.size()); + return R.ok(users); + + } catch (Exception e) { + log.error("【钉钉部门Controller】获取部门员工失败", e); + return R.error("获取部门员工失败: " + e.getMessage()); + } + } + + /** + * 构建部门树 + * + * @param allDepts 所有部门 + * @param parentId 父部门ID + * @return 部门树 + */ + private List> buildTree(List allDepts, Long parentId) { + List> tree = new ArrayList<>(); + + for (DingtalkDepartmentEntity dept : allDepts) { + if (dept.getParentId().equals(parentId)) { + Map node = new HashMap<>(); + node.put("id", dept.getId()); + node.put("deptId", dept.getDeptId()); + node.put("name", dept.getName()); + node.put("parentId", dept.getParentId()); + node.put("deptOrder", dept.getDeptOrder()); + node.put("createTime", dept.getCreateTime()); + node.put("updateTime", dept.getUpdateTime()); + + // 递归获取子部门 + List> children = buildTree(allDepts, dept.getDeptId()); + if (!children.isEmpty()) { + node.put("children", children); + } + + tree.add(node); + } + } + + return tree; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/controller/admin/AdminDingtalkSyncController.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/controller/admin/AdminDingtalkSyncController.java new file mode 100644 index 0000000..99d2792 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/controller/admin/AdminDingtalkSyncController.java @@ -0,0 +1,92 @@ +package com.cool.modules.dingtalk.controller.admin; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.dingtalk.entity.DingtalkSyncLogEntity; +import com.cool.modules.dingtalk.service.DingtalkSyncLogService; +import com.cool.modules.dingtalk.service.DingtalkSyncService; +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 lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import static com.cool.modules.dingtalk.entity.table.DingtalkSyncLogEntityTableDef.DINGTALK_SYNC_LOG_ENTITY; + +/** + * 钉钉同步管理Controller + */ +@Slf4j +@Tag(name = "钉钉同步", description = "钉钉同步管理") +@CoolRestController(api = {"page", "info"}) +@RequiredArgsConstructor +public class AdminDingtalkSyncController extends BaseController { + + private final DingtalkSyncService dingtalkSyncService; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + log.info("【钉钉同步日志Controller】接收到分页请求参数: {}", requestParams); + + // 构建查询条件 + QueryWrapper qw = QueryWrapper.create() + .orderBy(DINGTALK_SYNC_LOG_ENTITY.CREATE_TIME, false); + + // 手动处理筛选条件 + if (requestParams.containsKey("syncType")) { + Integer syncType = requestParams.getInt("syncType"); + log.info("【钉钉同步日志Controller】添加syncType筛选条件: {}", syncType); + qw.and(DINGTALK_SYNC_LOG_ENTITY.SYNC_TYPE.eq(syncType)); + } + + if (requestParams.containsKey("syncStatus")) { + Integer syncStatus = requestParams.getInt("syncStatus"); + log.info("【钉钉同步日志Controller】添加syncStatus筛选条件: {}", syncStatus); + qw.and(DINGTALK_SYNC_LOG_ENTITY.SYNC_STATUS.eq(syncStatus)); + } + + setPageOption(createOp().queryWrapper(qw)); + + log.info("【钉钉同步日志Controller】查询条件构建完成"); + } + + @Operation(summary = "手动触发同步", description = "手动触发钉钉组织架构同步") + @PostMapping("/trigger") + public R triggerSync() { + log.info("【钉钉同步Controller】手动触发同步"); + + try { + // 执行同步(类型为手动:0)异步执行 + dingtalkSyncService.syncAll(0); + + log.info("【钉钉同步Controller】同步任务已启动"); + return R.ok("同步任务已启动,请稍后查看同步日志"); + + } catch (Exception e) { + log.error("【钉钉同步Controller】触发同步失败", e); + return R.error("触发同步失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取同步状态", description = "检查当前同步任务状态") + @GetMapping("/status") + public R getSyncStatus() { + log.info("【钉钉同步Controller】获取同步状态"); + + try { + JSONObject status = dingtalkSyncService.checkSyncStatus(); + log.info("【钉钉同步Controller】同步状态: {}", status); + return R.ok(status); + } catch (Exception e) { + log.error("【钉钉同步Controller】获取同步状态失败", e); + return R.error("获取同步状态失败: " + e.getMessage()); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/entity/DingtalkConfigEntity.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/entity/DingtalkConfigEntity.java new file mode 100644 index 0000000..4e0c9a7 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/entity/DingtalkConfigEntity.java @@ -0,0 +1,38 @@ +package com.cool.modules.dingtalk.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import lombok.Getter; +import lombok.Setter; + +/** + * 钉钉配置实体 + * 用于存储钉钉企业内部应用的配置信息 + */ +@Getter +@Setter +@Table(value = "dingtalk_config", comment = "钉钉配置表") +public class DingtalkConfigEntity extends BaseEntity { + + @ColumnDefine(comment = "钉钉应用AppKey", notNull = true, length = 100) + private String appKey; + + @ColumnDefine(comment = "钉钉应用AppSecret(加密存储)", notNull = true, length = 200) + private String appSecret; + + @ColumnDefine(comment = "企业ID(corpId)", notNull = true, length = 100) + private String corpId; + + @ColumnDefine(comment = "定时同步Cron表达式", defaultValue = "0 0 2 * * ?", length = 50) + private String syncCron; + + @ColumnDefine(comment = "是否启用自动同步 0-否 1-是", defaultValue = "1") + private Integer autoSync; + + @ColumnDefine(comment = "同步状态 0-空闲 1-同步中", defaultValue = "0") + private Integer syncStatus; + + @ColumnDefine(comment = "配置状态 0-禁用 1-启用", defaultValue = "1") + private Integer status; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/entity/DingtalkDepartmentEntity.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/entity/DingtalkDepartmentEntity.java new file mode 100644 index 0000000..97b6dc5 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/entity/DingtalkDepartmentEntity.java @@ -0,0 +1,37 @@ +package com.cool.modules.dingtalk.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import com.tangzc.mybatisflex.autotable.annotation.UniIndex; +import lombok.Getter; +import lombok.Setter; + +/** + * 钉钉部门实体 + * 用于存储从钉钉同步的部门信息 + */ +@Getter +@Setter +@Table(value = "dingtalk_department", comment = "钉钉部门表") +public class DingtalkDepartmentEntity extends BaseEntity { + + @UniIndex + @ColumnDefine(comment = "钉钉部门ID", notNull = true) + private Long deptId; + + @ColumnDefine(comment = "部门名称", notNull = true, length = 200) + private String name; + + @ColumnDefine(comment = "父部门ID(钉钉ID)", defaultValue = "0") + private Long parentId; + + @ColumnDefine(comment = "排序号", defaultValue = "0") + private Integer deptOrder; + + @ColumnDefine(comment = "是否创建部门群", defaultValue = "false") + private Boolean createDeptGroup; + + @ColumnDefine(comment = "是否自动加入部门群", defaultValue = "false") + private Boolean autoAddUser; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/entity/DingtalkSyncLogEntity.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/entity/DingtalkSyncLogEntity.java new file mode 100644 index 0000000..9505a8d --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/entity/DingtalkSyncLogEntity.java @@ -0,0 +1,67 @@ +package com.cool.modules.dingtalk.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +/** + * 钉钉同步日志实体 + * 用于记录每次同步的详细信息和结果 + */ +@Getter +@Setter +@Table(value = "dingtalk_sync_log", comment = "钉钉同步日志表") +public class DingtalkSyncLogEntity extends BaseEntity { + + @ColumnDefine(comment = "同步类型 0-手动 1-自动", notNull = true) + private Integer syncType; + + @ColumnDefine(comment = "同步状态 0-失败 1-成功 2-部分成功", notNull = true) + private Integer syncStatus; + + @ColumnDefine(comment = "部门同步成功数量", defaultValue = "0") + private Integer deptSuccessCount; + + @ColumnDefine(comment = "部门同步失败数量", defaultValue = "0") + private Integer deptFailCount; + + @ColumnDefine(comment = "用户同步成功数量", defaultValue = "0") + private Integer userSuccessCount; + + @ColumnDefine(comment = "用户同步失败数量", defaultValue = "0") + private Integer userFailCount; + + @ColumnDefine(comment = "错误信息", type = "TEXT") + private String errorMsg; + + @ColumnDefine(comment = "耗时(毫秒)", defaultValue = "0") + private Integer costTime; + + /** + * 获取同步类型文本(用于前端显示) + */ + @JsonProperty("syncTypeText") + public String getSyncTypeText() { + return syncType != null && syncType == 0 ? "手动" : "自动"; + } + + /** + * 获取同步状态文本(用于前端显示) + */ + @JsonProperty("syncStatusText") + public String getSyncStatusText() { + if (syncStatus == null) { + return "未知"; + } + if (syncStatus == 1) { + return "成功"; + } else if (syncStatus == 2) { + return "部分成功"; + } else { + return "失败"; + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/mapper/DingtalkConfigMapper.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/mapper/DingtalkConfigMapper.java new file mode 100644 index 0000000..58ec087 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/mapper/DingtalkConfigMapper.java @@ -0,0 +1,12 @@ +package com.cool.modules.dingtalk.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.dingtalk.entity.DingtalkConfigEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 钉钉配置Mapper + */ +@Mapper +public interface DingtalkConfigMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/mapper/DingtalkDepartmentMapper.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/mapper/DingtalkDepartmentMapper.java new file mode 100644 index 0000000..702760a --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/mapper/DingtalkDepartmentMapper.java @@ -0,0 +1,12 @@ +package com.cool.modules.dingtalk.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.dingtalk.entity.DingtalkDepartmentEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 钉钉部门Mapper + */ +@Mapper +public interface DingtalkDepartmentMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/mapper/DingtalkSyncLogMapper.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/mapper/DingtalkSyncLogMapper.java new file mode 100644 index 0000000..5661530 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/mapper/DingtalkSyncLogMapper.java @@ -0,0 +1,12 @@ +package com.cool.modules.dingtalk.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.dingtalk.entity.DingtalkSyncLogEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 钉钉同步日志Mapper + */ +@Mapper +public interface DingtalkSyncLogMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkConfigService.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkConfigService.java new file mode 100644 index 0000000..7f4fdbd --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkConfigService.java @@ -0,0 +1,34 @@ +package com.cool.modules.dingtalk.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.dingtalk.entity.DingtalkConfigEntity; + +/** + * 钉钉配置服务接口 + */ +public interface DingtalkConfigService extends BaseService { + + /** + * 获取钉钉配置(单例配置,只有一条记录) + * + * @return 钉钉配置 + */ + DingtalkConfigEntity getConfig(); + + /** + * 验证配置有效性(测试AccessToken获取) + * + * @param appKey AppKey + * @param appSecret AppSecret + * @return 是否有效 + */ + boolean validateConfig(String appKey, String appSecret); + + /** + * 保存或更新配置 + * + * @param entity 配置实体 + * @return 保存后的配置 + */ + DingtalkConfigEntity saveOrUpdateConfig(DingtalkConfigEntity entity); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkDepartmentService.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkDepartmentService.java new file mode 100644 index 0000000..c068aab --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkDepartmentService.java @@ -0,0 +1,10 @@ +package com.cool.modules.dingtalk.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.dingtalk.entity.DingtalkDepartmentEntity; + +/** + * 钉钉部门服务接口 + */ +public interface DingtalkDepartmentService extends BaseService { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkSyncLogService.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkSyncLogService.java new file mode 100644 index 0000000..ac4f3e2 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkSyncLogService.java @@ -0,0 +1,10 @@ +package com.cool.modules.dingtalk.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.dingtalk.entity.DingtalkSyncLogEntity; + +/** + * 钉钉同步日志服务接口 + */ +public interface DingtalkSyncLogService extends BaseService { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkSyncService.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkSyncService.java new file mode 100644 index 0000000..6eac101 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/DingtalkSyncService.java @@ -0,0 +1,24 @@ +package com.cool.modules.dingtalk.service; + +import cn.hutool.json.JSONObject; + +/** + * 钉钉同步服务接口 + */ +public interface DingtalkSyncService { + + /** + * 执行完整同步(部门+用户) + * 异步执行,不等待结果 + * + * @param syncType 同步类型 0-手动 1-自动 + */ + void syncAll(Integer syncType); + + /** + * 检查同步状态 + * + * @return 同步状态信息 + */ + JSONObject checkSyncStatus(); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkConfigServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkConfigServiceImpl.java new file mode 100644 index 0000000..67e7ff3 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkConfigServiceImpl.java @@ -0,0 +1,89 @@ +package com.cool.modules.dingtalk.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.dingtalk.entity.DingtalkConfigEntity; +import com.cool.modules.dingtalk.mapper.DingtalkConfigMapper; +import com.cool.modules.dingtalk.service.DingtalkConfigService; +import com.cool.modules.dingtalk.util.DingtalkApiUtil; +import com.mybatisflex.core.query.QueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 钉钉配置服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DingtalkConfigServiceImpl extends BaseServiceImpl + implements DingtalkConfigService { + + private final DingtalkApiUtil dingtalkApiUtil; + + @Override + public DingtalkConfigEntity getConfig() { + log.info("【钉钉配置】获取钉钉配置"); + + QueryWrapper qw = QueryWrapper.create() + .eq(DingtalkConfigEntity::getStatus, 1) + .orderBy(DingtalkConfigEntity::getId, true) + .limit(1); + + DingtalkConfigEntity config = this.getOne(qw); + + if (config != null) { + log.info("【钉钉配置】配置获取成功, ID: {}, AppKey: {}", config.getId(), config.getAppKey()); + } else { + log.warn("【钉钉配置】未找到启用的配置"); + } + + return config; + } + + @Override + public boolean validateConfig(String appKey, String appSecret) { + log.info("【钉钉配置】验证配置有效性, AppKey: {}", appKey); + + try { + // 临时设置配置并尝试获取AccessToken + dingtalkApiUtil.setConfig(appKey, appSecret); + String accessToken = dingtalkApiUtil.getAccessToken(); + + if (accessToken != null && !accessToken.isEmpty()) { + log.info("【钉钉配置】配置验证成功, AccessToken: {}", accessToken.substring(0, Math.min(20, accessToken.length())) + "..."); + return true; + } else { + log.error("【钉钉配置】配置验证失败, AccessToken为空"); + return false; + } + } catch (Exception e) { + log.error("【钉钉配置】配置验证异常", e); + return false; + } + } + + @Override + public DingtalkConfigEntity saveOrUpdateConfig(DingtalkConfigEntity entity) { + log.info("【钉钉配置】保存或更新配置, AppKey: {}", entity.getAppKey()); + + // 查询是否已存在配置 + DingtalkConfigEntity existingConfig = getConfig(); + + if (existingConfig != null) { + // 更新现有配置 + entity.setId(existingConfig.getId()); + this.updateById(entity); + log.info("【钉钉配置】更新配置成功, ID: {}", entity.getId()); + } else { + // 新增配置 + this.save(entity); + log.info("【钉钉配置】新增配置成功, ID: {}", entity.getId()); + } + + // 更新DingtalkApiUtil中的配置 + dingtalkApiUtil.setConfig(entity.getAppKey(), entity.getAppSecret()); + + return entity; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkDepartmentServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkDepartmentServiceImpl.java new file mode 100644 index 0000000..1ea62e5 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkDepartmentServiceImpl.java @@ -0,0 +1,17 @@ +package com.cool.modules.dingtalk.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.dingtalk.entity.DingtalkDepartmentEntity; +import com.cool.modules.dingtalk.mapper.DingtalkDepartmentMapper; +import com.cool.modules.dingtalk.service.DingtalkDepartmentService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 钉钉部门服务实现 + */ +@Slf4j +@Service +public class DingtalkDepartmentServiceImpl extends BaseServiceImpl + implements DingtalkDepartmentService { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkSyncLogServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkSyncLogServiceImpl.java new file mode 100644 index 0000000..dc2587c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkSyncLogServiceImpl.java @@ -0,0 +1,18 @@ +package com.cool.modules.dingtalk.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.dingtalk.entity.DingtalkSyncLogEntity; +import com.cool.modules.dingtalk.mapper.DingtalkSyncLogMapper; +import com.cool.modules.dingtalk.service.DingtalkSyncLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 钉钉同步日志服务实现 + */ +@Slf4j +@Service +public class DingtalkSyncLogServiceImpl extends BaseServiceImpl + implements DingtalkSyncLogService { +} + diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkSyncServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkSyncServiceImpl.java new file mode 100644 index 0000000..ee29967 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/service/impl/DingtalkSyncServiceImpl.java @@ -0,0 +1,439 @@ +package com.cool.modules.dingtalk.service.impl; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.modules.dingtalk.entity.*; +import com.cool.modules.dingtalk.mapper.*; +import com.cool.modules.dingtalk.service.DingtalkConfigService; +import com.cool.modules.dingtalk.service.DingtalkSyncService; +import com.cool.modules.dingtalk.util.DingtalkApiUtil; +import com.cool.modules.user.entity.UserInfoEntity; +import com.cool.modules.user.mapper.UserInfoMapper; +import com.mybatisflex.core.query.QueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +/** + * 钉钉同步服务实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DingtalkSyncServiceImpl implements DingtalkSyncService { + + private final DingtalkConfigService dingtalkConfigService; + private final DingtalkApiUtil dingtalkApiUtil; + private final DingtalkDepartmentMapper departmentMapper; + private final DingtalkSyncLogMapper syncLogMapper; + private final UserInfoMapper userInfoMapper; + + @Override + @Async + public void syncAll(Integer syncType) { + log.info("【钉钉同步】开始执行完整同步, 同步类型: {}", syncType == 0 ? "手动" : "自动"); + + long startTime = System.currentTimeMillis(); + + // 创建同步日志 + DingtalkSyncLogEntity syncLog = new DingtalkSyncLogEntity(); + syncLog.setSyncType(syncType); + syncLog.setSyncStatus(0); // 初始状态:失败 + syncLog.setDeptSuccessCount(0); + syncLog.setDeptFailCount(0); + syncLog.setUserSuccessCount(0); + syncLog.setUserFailCount(0); + + try { + // 检查配置 + DingtalkConfigEntity config = dingtalkConfigService.getConfig(); + if (config == null) { + throw new RuntimeException("钉钉配置未完成"); + } + + // 检查同步状态 + if (config.getSyncStatus() == 1) { + log.warn("【钉钉同步】同步任务正在执行中,拒绝新任务"); + throw new RuntimeException("同步任务正在执行中,请稍后再试"); + } + + // 更新同步状态为"同步中" + config.setSyncStatus(1); + dingtalkConfigService.updateById(config); + + // 初始化API工具 + dingtalkApiUtil.setConfig(config.getAppKey(), config.getAppSecret()); + + // 同步部门 + int[] deptResult = syncDepartments(); + syncLog.setDeptSuccessCount(deptResult[0]); + syncLog.setDeptFailCount(deptResult[1]); + + // 同步用户 + int[] userResult = syncUsers(); + syncLog.setUserSuccessCount(userResult[0]); + syncLog.setUserFailCount(userResult[1]); + + // 判断同步状态 + if (syncLog.getDeptFailCount() == 0 && syncLog.getUserFailCount() == 0) { + syncLog.setSyncStatus(1); // 完全成功 + log.info("【钉钉同步】同步完全成功"); + } else if (syncLog.getDeptSuccessCount() > 0 || syncLog.getUserSuccessCount() > 0) { + syncLog.setSyncStatus(2); // 部分成功 + log.warn("【钉钉同步】同步部分成功"); + } else { + syncLog.setSyncStatus(0); // 失败 + log.error("【钉钉同步】同步失败"); + } + + } catch (Exception e) { + log.error("【钉钉同步】同步过程异常", e); + syncLog.setSyncStatus(0); + syncLog.setErrorMsg(e.getMessage()); + } finally { + // 计算耗时 + long costTime = System.currentTimeMillis() - startTime; + syncLog.setCostTime((int) costTime); + + // 保存同步日志 + syncLogMapper.insert(syncLog); + log.info("【钉钉同步】同步完成, 耗时: {}ms, 日志ID: {}", costTime, syncLog.getId()); + + // 重置同步状态 + DingtalkConfigEntity config = dingtalkConfigService.getConfig(); + if (config != null) { + config.setSyncStatus(0); + dingtalkConfigService.updateById(config); + } + } + } + + /** + * 同步部门信息 + * + * @return [成功数量, 失败数量] + */ + @Transactional + public int[] syncDepartments() { + log.info("【钉钉同步】开始同步部门"); + + int successCount = 0; + int failCount = 0; + + try { + // 获取所有部门(递归) + List allDepts = new ArrayList<>(); + getAllDepartments(1L, allDepts); // 从根部门开始 + + log.info("【钉钉同步】获取到 {} 个部门", allDepts.size()); + + // 删除本地所有部门(全量同步) + // 添加 where id >= 0 条件以满足 MyBatis-Flex 的安全限制 + QueryWrapper deleteQw = QueryWrapper.create() + .where(DingtalkDepartmentEntity::getId).ge(0); + int deletedCount = departmentMapper.deleteByQuery(deleteQw); + log.info("【钉钉同步】删除旧部门数据 {} 条", deletedCount); + + // 批量插入新部门 + for (DingtalkDepartmentEntity dept : allDepts) { + try { + departmentMapper.insert(dept); + successCount++; + } catch (Exception e) { + log.error("【钉钉同步】插入部门失败: {}", dept.getName(), e); + failCount++; + } + } + + log.info("【钉钉同步】部门同步完成, 成功: {}, 失败: {}", successCount, failCount); + + } catch (Exception e) { + log.error("【钉钉同步】部门同步异常", e); + failCount++; + } + + return new int[]{successCount, failCount}; + } + + /** + * 递归获取所有部门 + */ + private void getAllDepartments(Long parentId, List allDepts) { + try { + JSONObject response = dingtalkApiUtil.getDepartmentListSub(parentId); + + // 检查响应是否成功 + Integer errcode = response.getInt("errcode"); + if (errcode == null || errcode != 0) { + String errmsg = response.getStr("errmsg", "未知错误"); + log.error("【钉钉同步】获取部门列表失败, errcode: {}, errmsg: {}", errcode, errmsg); + return; + } + + // 钉钉API返回的result直接就是部门数组 + JSONArray deptList = response.getJSONArray("result"); + if (deptList == null || deptList.isEmpty()) { + log.info("【钉钉同步】部门ID {} 下无子部门", parentId); + return; + } + + log.info("【钉钉同步】部门ID {} 下有 {} 个子部门", parentId, deptList.size()); + + for (Object obj : deptList) { + JSONObject deptJson = (JSONObject) obj; + + DingtalkDepartmentEntity dept = new DingtalkDepartmentEntity(); + dept.setDeptId(deptJson.getLong("dept_id")); + dept.setName(deptJson.getStr("name")); + dept.setParentId(deptJson.getLong("parent_id", 0L)); + dept.setDeptOrder(deptJson.getInt("order", 0)); + dept.setCreateDeptGroup(deptJson.getBool("create_dept_group", false)); + dept.setAutoAddUser(deptJson.getBool("auto_add_user", false)); + + allDepts.add(dept); + log.info("【钉钉同步】添加部门: id={}, name={}", dept.getDeptId(), dept.getName()); + + // 递归获取子部门 + getAllDepartments(dept.getDeptId(), allDepts); + } + + } catch (Exception e) { + log.error("【钉钉同步】获取部门列表异常, parentId: {}", parentId, e); + } + } + + /** + * 同步用户信息 + * + * @return [成功数量, 失败数量] + */ + @Transactional + public int[] syncUsers() { + log.info("【钉钉同步】开始同步用户"); + + int successCount = 0; + int failCount = 0; + + try { + // 获取所有部门 + List depts = departmentMapper.selectAll(); + log.info("【钉钉同步】共 {} 个部门需要同步用户", depts.size()); + + Set syncedUserIds = new HashSet<>(); + + // 遍历每个部门获取用户 + for (DingtalkDepartmentEntity dept : depts) { + try { + List userIds = getUserIdsByDept(dept.getDeptId()); + + for (String userId : userIds) { + if (syncedUserIds.contains(userId)) { + continue; // 跳过已同步的用户 + } + + try { + syncUserDetail(userId, dept.getDeptId()); + syncedUserIds.add(userId); + successCount++; + } catch (Exception e) { + log.error("【钉钉同步】同步用户失败, userId: {}", userId, e); + failCount++; + } + } + + } catch (Exception e) { + log.error("【钉钉同步】获取部门用户失败, deptId: {}", dept.getDeptId(), e); + } + } + + // 删除钉钉中已不存在的用户 + deleteNonExistUsers(syncedUserIds); + + log.info("【钉钉同步】用户同步完成, 成功: {}, 失败: {}", successCount, failCount); + + } catch (Exception e) { + log.error("【钉钉同步】用户同步异常", e); + failCount++; + } + + return new int[]{successCount, failCount}; + } + + /** + * 获取部门下所有用户ID(支持分页) + */ + private List getUserIdsByDept(Long deptId) { + List userIds = new ArrayList<>(); + long cursor = 0; + int size = 100; + boolean hasMore = true; + + while (hasMore) { + try { + JSONObject response = dingtalkApiUtil.getUserList(deptId, cursor, size); + + // 检查响应是否成功 + Integer errcode = response.getInt("errcode"); + if (errcode == null || errcode != 0) { + String errmsg = response.getStr("errmsg", "未知错误"); + log.error("【钉钉同步】获取部门用户列表失败, deptId: {}, errcode: {}, errmsg: {}", deptId, errcode, errmsg); + break; + } + + JSONObject result = response.getJSONObject("result"); + if (result == null) { + log.warn("【钉钉同步】部门用户列表结果为空, deptId: {}", deptId); + break; + } + + JSONArray userList = result.getJSONArray("list"); + if (userList != null && !userList.isEmpty()) { + for (Object obj : userList) { + JSONObject userJson = (JSONObject) obj; + userIds.add(userJson.getStr("userid")); + } + log.info("【钉钉同步】部门 {} 本页获取 {} 个用户", deptId, userList.size()); + } + + hasMore = result.getBool("has_more", false); + cursor = result.getLong("next_cursor", 0L); + + } catch (Exception e) { + log.error("【钉钉同步】获取部门用户列表异常, deptId: {}", deptId, e); + break; + } + } + + log.info("【钉钉同步】部门 {} 共获取 {} 个用户", deptId, userIds.size()); + return userIds; + } + + /** + * 同步用户详情 + */ + private void syncUserDetail(String dingtalkUserId, Long deptId) { + JSONObject response = dingtalkApiUtil.getUserDetail(dingtalkUserId); + + // 检查响应是否成功 + Integer errcode = response.getInt("errcode"); + if (errcode == null || errcode != 0) { + String errmsg = response.getStr("errmsg", "未知错误"); + log.error("【钉钉同步】获取用户详情失败, userId: {}, errcode: {}, errmsg: {}", dingtalkUserId, errcode, errmsg); + return; + } + + JSONObject result = response.getJSONObject("result"); + if (result == null) { + log.warn("【钉钉同步】用户详情为空, userId: {}", dingtalkUserId); + return; + } + + // 查询本地是否已存在该用户 + QueryWrapper qw = QueryWrapper.create() + .eq(UserInfoEntity::getDingtalkUserId, dingtalkUserId); + UserInfoEntity user = userInfoMapper.selectOneByQuery(qw); + + boolean isNew = (user == null); + if (isNew) { + user = new UserInfoEntity(); + } + + // 更新用户信息 + user.setDingtalkUserId(dingtalkUserId); + user.setDingtalkDeptId(deptId); + user.setNickName(result.getStr("name")); + user.setPhone(result.getStr("mobile")); + user.setJobNumber(result.getStr("job_number")); + user.setPosition(result.getStr("title")); + user.setAvatarUrl(result.getStr("avatar")); + + if (isNew) { + userInfoMapper.insert(user); + log.info("【钉钉同步】新增用户: {}", user.getNickName()); + } else { + userInfoMapper.update(user); + log.info("【钉钉同步】更新用户: {}", user.getNickName()); + } + } + + /** + * 删除钉钉中已不存在的用户 + */ + private void deleteNonExistUsers(Set syncedUserIds) { + QueryWrapper qw = QueryWrapper.create() + .isNotNull(UserInfoEntity::getDingtalkUserId); + List allUsers = userInfoMapper.selectListByQuery(qw); + + int deleteCount = 0; + for (UserInfoEntity user : allUsers) { + if (!syncedUserIds.contains(user.getDingtalkUserId())) { + userInfoMapper.deleteById(user.getId()); + deleteCount++; + log.info("【钉钉同步】删除用户: {}", user.getNickName()); + } + } + + log.info("【钉钉同步】删除 {} 个不存在的用户", deleteCount); + } + + @Override + public JSONObject checkSyncStatus() { + log.info("【钉钉同步】检查同步状态"); + + JSONObject result = JSONUtil.createObj(); + + // 获取配置信息 + DingtalkConfigEntity config = dingtalkConfigService.getConfig(); + if (config != null) { + // 当前是否正在同步 + result.set("isSyncing", config.getSyncStatus() == 1); + + // 下次同步时间(如果启用了自动同步) + if (config.getAutoSync() == 1 && config.getSyncCron() != null) { + result.set("nextSyncTime", "根据Cron表达式: " + config.getSyncCron()); + } else { + result.set("nextSyncTime", "未启用自动同步"); + } + } else { + result.set("isSyncing", false); + result.set("nextSyncTime", "未配置"); + } + + // 获取最新的同步日志 + QueryWrapper qw = QueryWrapper.create() + .orderBy(DingtalkSyncLogEntity::getCreateTime, false) + .limit(1); + DingtalkSyncLogEntity latestLog = syncLogMapper.selectOneByQuery(qw); + + if (latestLog != null) { + // 上次同步时间 + result.set("lastSyncTime", latestLog.getCreateTime()); + + // 最新同步日志详情 + JSONObject logDetail = JSONUtil.createObj(); + logDetail.set("id", latestLog.getId()); + logDetail.set("syncType", latestLog.getSyncType()); + logDetail.set("syncStatus", latestLog.getSyncStatus()); + logDetail.set("deptSuccessCount", latestLog.getDeptSuccessCount()); + logDetail.set("deptFailCount", latestLog.getDeptFailCount()); + logDetail.set("userSuccessCount", latestLog.getUserSuccessCount()); + logDetail.set("userFailCount", latestLog.getUserFailCount()); + logDetail.set("errorMsg", latestLog.getErrorMsg()); + logDetail.set("costTime", latestLog.getCostTime()); + logDetail.set("createTime", latestLog.getCreateTime()); + + result.set("latestLog", logDetail); + } else { + result.set("lastSyncTime", "暂无同步记录"); + result.set("latestLog", null); + } + + log.info("【钉钉同步】同步状态: {}", result); + return result; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/task/DingtalkSyncTask.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/task/DingtalkSyncTask.java new file mode 100644 index 0000000..00a1854 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/task/DingtalkSyncTask.java @@ -0,0 +1,53 @@ +package com.cool.modules.dingtalk.task; + +import com.cool.modules.dingtalk.entity.DingtalkConfigEntity; +import com.cool.modules.dingtalk.service.DingtalkConfigService; +import com.cool.modules.dingtalk.service.DingtalkSyncService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 钉钉同步定时任务 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class DingtalkSyncTask { + + private final DingtalkConfigService dingtalkConfigService; + private final DingtalkSyncService dingtalkSyncService; + + /** + * 定时自动同步 + * 默认每天凌晨2点执行 + */ + @Scheduled(cron = "${dingtalk.sync.cron:0 0 2 * * ?}") + public void autoSync() { + log.info("【钉钉定时任务】定时同步任务触发"); + + try { + // 获取配置 + DingtalkConfigEntity config = dingtalkConfigService.getConfig(); + + if (config == null) { + log.warn("【钉钉定时任务】钉钉配置不存在,跳过同步"); + return; + } + + if (config.getAutoSync() == null || config.getAutoSync() != 1) { + log.info("【钉钉定时任务】自动同步未启用,跳过同步"); + return; + } + + // 执行同步(类型为自动:1) + dingtalkSyncService.syncAll(1); + + log.info("【钉钉定时任务】定时同步任务已启动"); + + } catch (Exception e) { + log.error("【钉钉定时任务】定时同步任务执行异常", e); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/dingtalk/util/DingtalkApiUtil.java b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/util/DingtalkApiUtil.java new file mode 100644 index 0000000..436c979 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/dingtalk/util/DingtalkApiUtil.java @@ -0,0 +1,242 @@ +package com.cool.modules.dingtalk.util; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 钉钉API工具类 + * 封装钉钉开放平台API调用,支持AccessToken管理、重试机制等 + */ +@Slf4j +@Component +public class DingtalkApiUtil { + + // 钉钉API基础URL + private static final String DINGTALK_API_BASE_URL = "https://oapi.dingtalk.com"; + + // AccessToken缓存 + private String accessToken; + private Long expiresTime; + + // 钉钉配置(从数据库读取后设置) + private String appKey; + private String appSecret; + + /** + * 设置钉钉配置 + * + * @param appKey 应用Key + * @param appSecret 应用Secret + */ + public void setConfig(String appKey, String appSecret) { + log.info("【钉钉API】设置钉钉配置, appKey: {}", appKey); + this.appKey = appKey; + this.appSecret = appSecret; + // 清空缓存的token + this.accessToken = null; + this.expiresTime = null; + } + + /** + * 获取AccessToken(支持自动刷新) + * + * @return AccessToken + */ + public synchronized String getAccessToken() { + log.info("【钉钉API】获取AccessToken, 当前token是否过期: {}", isExpiringSoon()); + + // 如果token不存在或即将过期,重新获取 + if (accessToken == null || isExpiringSoon()) { + refreshAccessToken(); + } + + return accessToken; + } + + /** + * 判断Token是否即将过期(剩余时间小于5分钟) + */ + private boolean isExpiringSoon() { + if (expiresTime == null) { + return true; + } + long currentTime = System.currentTimeMillis(); + long remainingTime = expiresTime - currentTime; + return remainingTime < TimeUnit.MINUTES.toMillis(5); + } + + /** + * 刷新AccessToken + */ + private void refreshAccessToken() { + log.info("【钉钉API】开始刷新AccessToken"); + + if (StrUtil.isBlank(appKey) || StrUtil.isBlank(appSecret)) { + log.error("【钉钉API】appKey或appSecret未配置"); + throw new RuntimeException("钉钉配置未完成,请先配置AppKey和AppSecret"); + } + + try { + String url = DINGTALK_API_BASE_URL + "/gettoken"; + Map params = new HashMap<>(); + params.put("appkey", appKey); + params.put("appsecret", appSecret); + + HttpResponse response = HttpRequest.get(url) + .form(params) + .timeout(10000) + .execute(); + + String body = response.body(); + log.info("【钉钉API】获取AccessToken响应: {}", body); + + JSONObject jsonObject = JSONUtil.parseObj(body); + Integer errcode = jsonObject.getInt("errcode"); + + if (errcode != null && errcode == 0) { + this.accessToken = jsonObject.getStr("access_token"); + Integer expiresIn = jsonObject.getInt("expires_in"); + // 计算过期时间(当前时间 + 过期秒数) + this.expiresTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiresIn); + + log.info("【钉钉API】AccessToken刷新成功, token: {}, 有效期: {}秒", accessToken, expiresIn); + } else { + String errmsg = jsonObject.getStr("errmsg", "未知错误"); + log.error("【钉钉API】获取AccessToken失败, errcode: {}, errmsg: {}", errcode, errmsg); + throw new RuntimeException("获取钉钉AccessToken失败: " + errmsg); + } + } catch (Exception e) { + log.error("【钉钉API】刷新AccessToken异常", e); + throw new RuntimeException("刷新钉钉AccessToken异常: " + e.getMessage(), e); + } + } + + /** + * 调用钉钉API(POST请求,支持重试) + * + * @param apiPath API路径(如 /topapi/v2/department/listsub) + * @param params 请求参数 + * @return API响应JSON对象 + */ + public JSONObject callApi(String apiPath, Map params) { + return callApi(apiPath, params, 3); + } + + /** + * 调用钉钉API(POST请求,支持重试) + * + * @param apiPath API路径 + * @param params 请求参数 + * @param retryTimes 重试次数 + * @return API响应JSON对象 + */ + private JSONObject callApi(String apiPath, Map params, int retryTimes) { + log.info("【钉钉API】调用API: {}, 参数: {}", apiPath, params); + + String token = getAccessToken(); + String url = DINGTALK_API_BASE_URL + apiPath + "?access_token=" + token; + + try { + // 将params转换为JSON字符串 + String jsonBody = JSONUtil.toJsonStr(params); + + HttpResponse response = HttpRequest.post(url) + .body(jsonBody) + .contentType("application/json") + .timeout(15000) + .execute(); + + String body = response.body(); + log.info("【钉钉API】API响应: {}", body); + + JSONObject jsonObject = JSONUtil.parseObj(body); + Integer errcode = jsonObject.getInt("errcode"); + + if (errcode != null && errcode == 0) { + return jsonObject; + } else if (errcode != null && errcode == 40014) { + // Token过期,重新获取token后重试 + log.warn("【钉钉API】AccessToken过期, 重新获取后重试"); + refreshAccessToken(); + if (retryTimes > 0) { + return callApi(apiPath, params, retryTimes - 1); + } + } else if (errcode != null && (errcode == 60011 || errcode == 60020)) { + // 请求频繁或系统繁忙,延迟重试 + log.warn("【钉钉API】请求频繁或系统繁忙, errcode: {}, 延迟后重试", errcode); + if (retryTimes > 0) { + try { + Thread.sleep(1000 * (4 - retryTimes)); // 指数退避 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return callApi(apiPath, params, retryTimes - 1); + } + } + + String errmsg = jsonObject.getStr("errmsg", "未知错误"); + log.error("【钉钉API】API调用失败, errcode: {}, errmsg: {}", errcode, errmsg); + throw new RuntimeException("钉钉API调用失败: " + errmsg); + + } catch (Exception e) { + log.error("【钉钉API】API调用异常: {}", apiPath, e); + if (retryTimes > 0) { + log.info("【钉钉API】重试调用, 剩余次数: {}", retryTimes); + return callApi(apiPath, params, retryTimes - 1); + } + throw new RuntimeException("钉钉API调用异常: " + e.getMessage(), e); + } + } + + /** + * 获取部门子部门列表 + * + * @param deptId 父部门ID(根部门传1) + * @return 部门列表JSON + */ + public JSONObject getDepartmentListSub(Long deptId) { + log.info("【钉钉API】获取部门子部门列表, deptId: {}", deptId); + Map params = new HashMap<>(); + params.put("dept_id", deptId); + return callApi("/topapi/v2/department/listsub", params); + } + + /** + * 获取部门用户列表(分页) + * + * @param deptId 部门ID + * @param cursor 分页游标(首次传0) + * @param size 每页大小(最大100) + * @return 用户列表JSON + */ + public JSONObject getUserList(Long deptId, Long cursor, Integer size) { + log.info("【钉钉API】获取部门用户列表, deptId: {}, cursor: {}, size: {}", deptId, cursor, size); + Map params = new HashMap<>(); + params.put("dept_id", deptId); + params.put("cursor", cursor); + params.put("size", size); + return callApi("/topapi/v2/user/list", params); + } + + /** + * 获取用户详情 + * + * @param userId 钉钉用户ID + * @return 用户详情JSON + */ + public JSONObject getUserDetail(String userId) { + log.info("【钉钉API】获取用户详情, userId: {}", userId); + Map params = new HashMap<>(); + params.put("userid", userId); + return callApi("/topapi/v2/user/get", params); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/controller/admin/AdminMonthlyReportController.java b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/controller/admin/AdminMonthlyReportController.java new file mode 100644 index 0000000..ead4bc1 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/controller/admin/AdminMonthlyReportController.java @@ -0,0 +1,85 @@ +package com.cool.modules.monthlyreport.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.monthlyreport.entity.MonthlyReportEntity; +import com.cool.modules.monthlyreport.service.MonthlyReportService; +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 lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import static com.cool.modules.monthlyreport.entity.table.MonthlyReportEntityTableDef.MONTHLY_REPORT_ENTITY; + +/** + * 管理端月报Controller + */ +@Slf4j +@Tag(name = "管理端月报管理", description = "管理端月报查询与统计") +@CoolRestController(api = {"page", "list", "info"}) +@RequiredArgsConstructor +public class AdminMonthlyReportController extends BaseController { + + private final MonthlyReportService monthlyReportService; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + QueryWrapper qw = QueryWrapper.create() + .orderBy(MONTHLY_REPORT_ENTITY.MONTH, false) + .orderBy(MONTHLY_REPORT_ENTITY.CREATE_TIME, false); + + // 月份范围筛选(startMonth、endMonth,格式 yyyy-MM) + if (requestParams != null) { + String startMonth = requestParams.getStr("startMonth"); + String endMonth = requestParams.getStr("endMonth"); + if (startMonth != null && !startMonth.isEmpty()) { + qw.ge(MonthlyReportEntity::getMonth, startMonth); + } + if (endMonth != null && !endMonth.isEmpty()) { + qw.le(MonthlyReportEntity::getMonth, endMonth); + } + } + + setPageOption(createOp() + .keyWordLikeFields(MONTHLY_REPORT_ENTITY.USER_EDITED_CONTENT) + .fieldEq(MONTHLY_REPORT_ENTITY.STATUS) + .queryWrapper(qw) + ); + } + + @Operation(summary = "获取月报提交统计") + @GetMapping("/statistics") + public R getStatistics( + @RequestParam(required = false) String startMonth, + @RequestParam(required = false) String endMonth + ) { + try { + QueryWrapper qw = QueryWrapper.create(); + if (startMonth != null && !startMonth.isEmpty()) { + qw.ge(MonthlyReportEntity::getMonth, startMonth); + } + if (endMonth != null && !endMonth.isEmpty()) { + qw.le(MonthlyReportEntity::getMonth, endMonth); + } + + long totalCount = monthlyReportService.count(qw); + long submitted = monthlyReportService.count(qw.clone().eq(MonthlyReportEntity::getStatus, 1)); + long draft = monthlyReportService.count(qw.clone().eq(MonthlyReportEntity::getStatus, 0)); + + JSONObject result = new JSONObject(); + result.set("totalCount", totalCount); + result.set("submittedCount", submitted); + result.set("draftCount", draft); + return R.ok(result); + } catch (Exception e) { + log.error("【月报统计】失败", e); + return R.error("统计失败: " + e.getMessage()); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/controller/app/AppMonthlyReportController.java b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/controller/app/AppMonthlyReportController.java new file mode 100644 index 0000000..2f738d1 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/controller/app/AppMonthlyReportController.java @@ -0,0 +1,245 @@ +package com.cool.modules.monthlyreport.controller.app; + +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.monthlyreport.entity.MonthlyReportEntity; +import com.cool.modules.monthlyreport.service.MonthlyReportAiService; +import com.cool.modules.monthlyreport.service.MonthlyReportService; +import com.cool.modules.weeklyreport.entity.WeeklyReportEntity; +import com.cool.modules.weeklyreport.service.WeeklyReportService; +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 lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.Comparator; +import java.util.List; + +/** + * 用户端月报Controller + */ +@Slf4j +@Tag(name = "用户端月报管理", description = "用户端月报生成、提交与查询") +@RestController +@RequestMapping("/app/monthlyreport/report") +@RequiredArgsConstructor +public class AppMonthlyReportController extends BaseController { + + private final MonthlyReportService monthlyReportService; + private final MonthlyReportAiService monthlyReportAiService; + private final WeeklyReportService weeklyReportService; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + // APP侧暂不使用通用分页 + } + + @Operation(summary = "从本月已提交周报聚合并AI格式化") + @PostMapping("/generateFromWeekly") + public R generateFromWeekly(@RequestBody JSONObject data) { + try { + Long userId = data.getLong("userId"); + String month = data.getStr("month"); // yyyy-MM + String templateKey = data.getStr("templateKey", "monthly_report_format"); + if (userId == null || month == null || month.isEmpty()) { + return R.error("userId与month不能为空"); + } + + YearMonth ym = YearMonth.parse(month); + LocalDate start = ym.atDay(1); + LocalDate end = ym.atEndOfMonth(); + + QueryWrapper qw = QueryWrapper.create() + .eq(WeeklyReportEntity::getUserId, userId) + .ge(WeeklyReportEntity::getWeekStartDate, start) + .le(WeeklyReportEntity::getWeekEndDate, end) + .eq(WeeklyReportEntity::getStatus, 1); + + List weeklies = weeklyReportService.list(qw); + if (weeklies == null || weeklies.isEmpty()) { + return R.error("本月没有已提交的周报"); + } + + weeklies.sort(Comparator.comparing(WeeklyReportEntity::getWeekStartDate)); + + StringBuilder source = new StringBuilder(); + for (WeeklyReportEntity r : weeklies) { + String content = r.getUserEditedContent(); + if (content == null || content.isEmpty()) { + content = r.getAiFormattedContent(); + } + if (content == null || content.isEmpty()) { + content = r.getOriginalText(); + } + if (content != null && !content.isEmpty()) { + source.append("# ") + .append(r.getWeekStartDate()).append(" ~ ").append(r.getWeekEndDate()) + .append("\n") + .append(content) + .append("\n\n"); + } + } + + if (source.length() == 0) { + return R.error("本月周报内容为空,无法生成月报"); + } + + String formatted = monthlyReportAiService.formatMonthlyReport( + source.toString(), templateKey, month + ); + + JSONObject result = new JSONObject(); + result.set("formattedContent", formatted); + result.set("length", formatted != null ? formatted.length() : 0); + result.set("sourceCount", weeklies.size()); + result.set("sourceContent", source.toString()); + return R.ok(result); + } catch (Exception e) { + log.error("【月报自动生成】失败", e); + return R.error("月报自动生成失败: " + e.getMessage()); + } + } + + @Operation(summary = "AI格式化月报") + @PostMapping("/aiFormat") + public R aiFormat(@RequestBody JSONObject data) { + try { + String originalText = data.getStr("originalText"); + String templateKey = data.getStr("templateKey", "monthly_report_format"); + String month = data.getStr("month"); + String formattedContent = monthlyReportAiService.formatMonthlyReport(originalText, templateKey, month); + JSONObject result = new JSONObject(); + result.set("formattedContent", formattedContent); + result.set("length", formattedContent != null ? formattedContent.length() : 0); + return R.ok(result); + } catch (Exception e) { + log.error("【月报AI格式化】失败", e); + return R.error("AI格式化失败: " + e.getMessage()); + } + } + + @Operation(summary = "保存草稿") + @PostMapping("/saveDraft") + public R saveDraft(@RequestBody JSONObject data) { + try { + Long userId = data.getLong("userId"); + String month = data.getStr("month"); + if (userId == null || month == null || month.isEmpty()) { + return R.error("userId与month不能为空"); + } + + QueryWrapper qw = QueryWrapper.create() + .eq(MonthlyReportEntity::getUserId, userId) + .eq(MonthlyReportEntity::getMonth, month); + MonthlyReportEntity existing = monthlyReportService.getOne(qw); + + if (existing == null) { + existing = new MonthlyReportEntity(); + existing.setUserId(userId); + existing.setMonth(month); + } + + existing.setOriginalText(data.getStr("originalText")); + existing.setAiFormattedContent(data.getStr("aiFormattedContent")); + existing.setUserEditedContent(data.getStr("userEditedContent")); + existing.setInputType(data.getInt("inputType", 0)); + existing.setStatus(0); + + if (existing.getId() == null) { + monthlyReportService.save(existing); + } else { + monthlyReportService.updateById(existing); + } + return R.ok(existing); + } catch (Exception e) { + log.error("【月报草稿】保存失败", e); + return R.error("保存失败: " + e.getMessage()); + } + } + + @Operation(summary = "提交月报") + @PostMapping("/submit") + public R submit(@RequestBody JSONObject data) { + try { + Long userId = data.getLong("userId"); + String month = data.getStr("month"); + if (userId == null || month == null || month.isEmpty()) { + return R.error("userId与month不能为空"); + } + + QueryWrapper qw = QueryWrapper.create() + .eq(MonthlyReportEntity::getUserId, userId) + .eq(MonthlyReportEntity::getMonth, month); + MonthlyReportEntity report = monthlyReportService.getOne(qw); + if (report == null) { + report = new MonthlyReportEntity(); + report.setUserId(userId); + report.setMonth(month); + } + report.setOriginalText(data.getStr("originalText")); + report.setAiFormattedContent(data.getStr("aiFormattedContent")); + report.setUserEditedContent(data.getStr("userEditedContent")); + report.setInputType(data.getInt("inputType", 0)); + report.setStatus(1); + report.setSubmitTime(LocalDateTime.now()); + + if (report.getId() == null) { + monthlyReportService.save(report); + } else { + monthlyReportService.updateById(report); + } + return R.ok(report); + } catch (Exception e) { + log.error("【月报提交】失败", e); + return R.error("提交失败: " + e.getMessage()); + } + } + + @Operation(summary = "月报详情") + @GetMapping("/detail") + public R detail(@RequestParam Long id, @RequestParam(required = false) Long userId) { + try { + MonthlyReportEntity report = monthlyReportService.getById(id); + return R.ok(report); + } catch (Exception e) { + log.error("【月报详情】失败", e); + return R.error("获取详情失败: " + e.getMessage()); + } + } + + @Operation(summary = "我的月报列表") + @GetMapping("/myReports") + public R myReports( + @RequestParam Long userId, + @RequestParam(required = false) Integer status, + @RequestParam(required = false, defaultValue = "1") Integer page, + @RequestParam(required = false, defaultValue = "20") Integer size + ) { + try { + QueryWrapper qw = QueryWrapper.create() + .eq(MonthlyReportEntity::getUserId, userId) + .orderBy(MonthlyReportEntity::getMonth, false) + .orderBy(MonthlyReportEntity::getCreateTime, false); + if (status != null) { + qw.eq(MonthlyReportEntity::getStatus, status); + } + + // 简单分页:偏移量分页 + int offset = (Math.max(page, 1) - 1) * Math.max(size, 1); + qw.limit(offset, Math.max(size, 1)); + List list = monthlyReportService.list(qw); + return R.ok(list); + } catch (Exception e) { + log.error("【我的月报列表】失败", e); + return R.error("加载失败: " + e.getMessage()); + } + } +} + diff --git a/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/entity/MonthlyReportEntity.java b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/entity/MonthlyReportEntity.java new file mode 100644 index 0000000..faf7ac3 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/entity/MonthlyReportEntity.java @@ -0,0 +1,43 @@ +package com.cool.modules.monthlyreport.entity; + +import com.cool.core.annotation.ColumnDefine; +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** + * 月报实体类 + */ +@Getter +@Setter +@Table(value = "monthly_report", comment = "月报表") +public class MonthlyReportEntity extends BaseEntity { + + @ColumnDefine(comment = "用户ID", notNull = true) + private Long userId; + + @ColumnDefine(comment = "月份,格式yyyy-MM", notNull = true) + private String month; + + @ColumnDefine(comment = "原始输入内容", type = "TEXT") + private String originalText; + + @ColumnDefine(comment = "AI格式化内容", type = "TEXT") + private String aiFormattedContent; + + @ColumnDefine(comment = "用户最终编辑内容", type = "TEXT") + private String userEditedContent; + + @ColumnDefine(comment = "状态 0-草稿 1-已提交", defaultValue = "0") + private Integer status; + + @ColumnDefine(comment = "输入方式 0-文字 1-语音", defaultValue = "0") + private Integer inputType; + + @ColumnDefine(comment = "提交时间") + private LocalDateTime submitTime; +} + diff --git a/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/mapper/MonthlyReportMapper.java b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/mapper/MonthlyReportMapper.java new file mode 100644 index 0000000..b83e32e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/mapper/MonthlyReportMapper.java @@ -0,0 +1,11 @@ +package com.cool.modules.monthlyreport.mapper; + +import com.cool.modules.monthlyreport.entity.MonthlyReportEntity; +import com.mybatisflex.core.BaseMapper; + +/** + * 月报Mapper + */ +public interface MonthlyReportMapper extends BaseMapper { +} + diff --git a/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/MonthlyReportAiService.java b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/MonthlyReportAiService.java new file mode 100644 index 0000000..052bd5a --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/MonthlyReportAiService.java @@ -0,0 +1,18 @@ +package com.cool.modules.monthlyreport.service; + +/** + * 月报AI格式化Service接口 + */ +public interface MonthlyReportAiService { + + /** + * 格式化月报内容 + * + * @param originalText 原始输入文本 + * @param templateKey 模板标识(默认: monthly_report_format) + * @param month 月份(yyyy-MM) + * @return 格式化后的月报内容 + */ + String formatMonthlyReport(String originalText, String templateKey, String month); +} + diff --git a/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/MonthlyReportService.java b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/MonthlyReportService.java new file mode 100644 index 0000000..3ecede9 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/MonthlyReportService.java @@ -0,0 +1,11 @@ +package com.cool.modules.monthlyreport.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.monthlyreport.entity.MonthlyReportEntity; + +/** + * 月报Service接口 + */ +public interface MonthlyReportService extends BaseService { +} + diff --git a/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/impl/MonthlyReportAiServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/impl/MonthlyReportAiServiceImpl.java new file mode 100644 index 0000000..10c9e8c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/impl/MonthlyReportAiServiceImpl.java @@ -0,0 +1,119 @@ +package com.cool.modules.monthlyreport.service.impl; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.modules.aiconfig.entity.AiApiConfigEntity; +import com.cool.modules.aiconfig.entity.AiModelEntity; +import com.cool.modules.aiconfig.entity.AiTemplateEntity; +import com.cool.modules.aiconfig.service.AiApiConfigService; +import com.cool.modules.aiconfig.service.AiModelService; +import com.cool.modules.aiconfig.service.AiTemplateService; +import com.cool.modules.monthlyreport.service.MonthlyReportAiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 月报AI格式化Service实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MonthlyReportAiServiceImpl implements MonthlyReportAiService { + + private final AiModelService aiModelService; + private final AiApiConfigService aiApiConfigService; + private final AiTemplateService aiTemplateService; + + @Override + public String formatMonthlyReport(String originalText, String templateKey, String month) { + String key = (templateKey == null || templateKey.isBlank()) ? "monthly_report_format" : templateKey; + log.info("【月报AI格式化】开始格式化, templateKey={}, originalText长度={}, month={}", + key, originalText != null ? originalText.length() : 0, month); + + try { + AiTemplateEntity template = aiTemplateService.getByTemplateKey(key); + if (template == null) { + throw new RuntimeException("月报模板未配置: " + key); + } + + AiModelEntity model = aiModelService.getById(template.getModelId()); + if (model == null) { + throw new RuntimeException("AI模型未配置"); + } + + String prompt = buildPrompt(template.getPromptContent(), originalText, month); + return callAiModel(model, prompt); + } catch (Exception e) { + log.error("【月报AI格式化】失败", e); + throw new RuntimeException("AI格式化失败: " + e.getMessage(), e); + } + } + + private String buildPrompt(String templateContent, String originalText, String month) { + String prompt = templateContent + .replace("{original_text}", originalText) + .replace("{{original_text}}", originalText) + .replace("{content}", originalText) + .replace("{{content}}", originalText); + + if (month != null && !month.isEmpty()) { + prompt = prompt + .replace("{month}", month) + .replace("{{month}}", month); + } + return prompt; + } + + private String callAiModel(AiModelEntity model, String prompt) { + try { + AiApiConfigEntity apiConfig = aiApiConfigService.getByModelId(model.getId()); + if (apiConfig == null) { + throw new RuntimeException("AI模型API配置不存在"); + } + + JSONObject requestBody = new JSONObject(); + requestBody.set("model", model.getModelKey()); + + JSONObject input = new JSONObject(); + JSONArray messages = new JSONArray(); + JSONObject message = new JSONObject(); + message.set("role", "user"); + message.set("content", prompt); + messages.add(message); + input.set("messages", messages); + requestBody.set("input", input); + + JSONObject parameters = new JSONObject(); + parameters.set("max_tokens", 2000); + requestBody.set("parameters", parameters); + + HttpRequest request = HttpRequest.post(apiConfig.getApiUrl()) + .header("Authorization", "Bearer " + apiConfig.getApiKey()) + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .timeout(120000); + + HttpResponse response = request.execute(); + String responseBody = response.body(); + if (!response.isOk()) { + throw new RuntimeException("AI模型调用失败: HTTP " + response.getStatus()); + } + + JSONObject result = JSONUtil.parseObj(responseBody); + String content = result.getByPath("output.text", String.class); + if (content == null || content.isEmpty()) { + content = result.getByPath("output.choices[0].message.content", String.class); + } + if (content == null || content.isEmpty()) { + throw new RuntimeException("AI模型返回内容为空"); + } + return content; + } catch (Exception e) { + throw new RuntimeException("AI模型调用失败: " + e.getMessage(), e); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/impl/MonthlyReportServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/impl/MonthlyReportServiceImpl.java new file mode 100644 index 0000000..9568675 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/monthlyreport/service/impl/MonthlyReportServiceImpl.java @@ -0,0 +1,21 @@ +package com.cool.modules.monthlyreport.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.monthlyreport.entity.MonthlyReportEntity; +import com.cool.modules.monthlyreport.mapper.MonthlyReportMapper; +import com.cool.modules.monthlyreport.service.MonthlyReportService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 月报Service实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MonthlyReportServiceImpl + extends BaseServiceImpl + implements MonthlyReportService { +} + diff --git a/cool-admin-java/src/main/java/com/cool/modules/recycle/aop/DeleteAspect.java b/cool-admin-java/src/main/java/com/cool/modules/recycle/aop/DeleteAspect.java new file mode 100644 index 0000000..45003ba --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/recycle/aop/DeleteAspect.java @@ -0,0 +1,112 @@ +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.core.util.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; + + @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.getAdminUsername()); + recycleDataEntity + .setUserId(Long.parseLong( + String.valueOf(CoolSecurityUtil.getAdminUserInfo(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/cool-admin-java/src/main/java/com/cool/modules/recycle/controller/admin/AdminRecycleDataController.java b/cool-admin-java/src/main/java/com/cool/modules/recycle/controller/admin/AdminRecycleDataController.java new file mode 100644 index 0000000..9e428e5 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/recycle/entity/RecycleDataEntity.java b/cool-admin-java/src/main/java/com/cool/modules/recycle/entity/RecycleDataEntity.java new file mode 100644 index 0000000..b268f6e --- /dev/null +++ b/cool-admin-java/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.cool.core.mybatis.handler.Fastjson2TypeHandler; +import com.cool.core.annotation.ColumnDefine; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Ignore; +import org.dromara.autotable.annotation.Index; + +/** + * 数据回收站 软删除的时候数据会回收到该表 + */ +@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/cool-admin-java/src/main/java/com/cool/modules/recycle/mapper/RecycleDataMapper.java b/cool-admin-java/src/main/java/com/cool/modules/recycle/mapper/RecycleDataMapper.java new file mode 100644 index 0000000..7fef289 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/recycle/service/RecycleDataService.java b/cool-admin-java/src/main/java/com/cool/modules/recycle/service/RecycleDataService.java new file mode 100644 index 0000000..612584e --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/recycle/service/impl/RecycleDataServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/recycle/service/impl/RecycleDataServiceImpl.java new file mode 100644 index 0000000..bc13286 --- /dev/null +++ b/cool-admin-java/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.create().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/cool-admin-java/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceInfoController.java b/cool-admin-java/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceInfoController.java new file mode 100644 index 0000000..af1ab9e --- /dev/null +++ b/cool-admin-java/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)); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceTypeController.java b/cool-admin-java/src/main/java/com/cool/modules/space/controller/admin/AdminSpaceTypeController.java new file mode 100644 index 0000000..d5c0ace --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/space/entity/SpaceInfoEntity.java b/cool-admin-java/src/main/java/com/cool/modules/space/entity/SpaceInfoEntity.java new file mode 100644 index 0000000..5216f1e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/space/entity/SpaceInfoEntity.java @@ -0,0 +1,57 @@ +package com.cool.modules.space.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Ignore; +import org.dromara.autotable.annotation.Index; + +/** + * 文件空间信息 + */ +@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") + 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/cool-admin-java/src/main/java/com/cool/modules/space/entity/SpaceTypeEntity.java b/cool-admin-java/src/main/java/com/cool/modules/space/entity/SpaceTypeEntity.java new file mode 100644 index 0000000..dafcc95 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/space/entity/SpaceTypeEntity.java @@ -0,0 +1,21 @@ +package com.cool.modules.space.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +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") + private Integer parentId; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/space/mapper/SpaceInfoMapper.java b/cool-admin-java/src/main/java/com/cool/modules/space/mapper/SpaceInfoMapper.java new file mode 100644 index 0000000..3f41f26 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/space/mapper/SpaceTypeMapper.java b/cool-admin-java/src/main/java/com/cool/modules/space/mapper/SpaceTypeMapper.java new file mode 100644 index 0000000..8858289 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/space/service/SpaceInfoService.java b/cool-admin-java/src/main/java/com/cool/modules/space/service/SpaceInfoService.java new file mode 100644 index 0000000..bbf358e --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/space/service/SpaceTypeService.java b/cool-admin-java/src/main/java/com/cool/modules/space/service/SpaceTypeService.java new file mode 100644 index 0000000..5f4aa5e --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/space/service/impl/SpaceInfoServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/space/service/impl/SpaceInfoServiceImpl.java new file mode 100644 index 0000000..de9ce6b --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/space/service/impl/SpaceTypeServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/space/service/impl/SpaceTypeServiceImpl.java new file mode 100644 index 0000000..3b5894f --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/task/config/ScheduleConfig.java b/cool-admin-java/src/main/java/com/cool/modules/task/config/ScheduleConfig.java new file mode 100644 index 0000000..020ab47 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/task/controller/admin/AdminTaskInfoController.java b/cool-admin-java/src/main/java/com/cool/modules/task/controller/admin/AdminTaskInfoController.java new file mode 100644 index 0000000..5d3eca4 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/task/controller/admin/AdminTaskInfoController.java @@ -0,0 +1,60 @@ +package com.cool.modules.task.controller.admin; + +import static com.cool.modules.task.entity.table.TaskInfoEntityTableDef.TASK_INFO_ENTITY; + +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.service.TaskInfoService; +import com.mybatisflex.core.paginate.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; + +/** + * 任务 + */ +@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/cool-admin-java/src/main/java/com/cool/modules/task/entity/TaskInfoEntity.java b/cool-admin-java/src/main/java/com/cool/modules/task/entity/TaskInfoEntity.java new file mode 100644 index 0000000..cba86c9 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/task/entity/TaskInfoEntity.java @@ -0,0 +1,62 @@ +package com.cool.modules.task.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import java.util.Date; +import lombok.Getter; +import lombok.Setter; + +@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") + 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/cool-admin-java/src/main/java/com/cool/modules/task/entity/TaskLogEntity.java b/cool-admin-java/src/main/java/com/cool/modules/task/entity/TaskLogEntity.java new file mode 100644 index 0000000..76ba620 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/task/entity/TaskLogEntity.java @@ -0,0 +1,29 @@ +package com.cool.modules.task.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; + +@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/cool-admin-java/src/main/java/com/cool/modules/task/event/TaskEvent.java b/cool-admin-java/src/main/java/com/cool/modules/task/event/TaskEvent.java new file mode 100644 index 0000000..1ec2012 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/task/event/TaskEvent.java @@ -0,0 +1,25 @@ +package com.cool.modules.task.event; + +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(ApplicationReadyEvent.class) + public void run() throws Exception { + taskInfoService.init(); + log.info("初始化任务"); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/task/mapper/TaskInfoMapper.java b/cool-admin-java/src/main/java/com/cool/modules/task/mapper/TaskInfoMapper.java new file mode 100644 index 0000000..770f7ea --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/task/mapper/TaskLogMapper.java b/cool-admin-java/src/main/java/com/cool/modules/task/mapper/TaskLogMapper.java new file mode 100644 index 0000000..10a0bce --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/task/run/ScheduleJob.java b/cool-admin-java/src/main/java/com/cool/modules/task/run/ScheduleJob.java new file mode 100644 index 0000000..f4fd06e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/task/run/ScheduleJob.java @@ -0,0 +1,107 @@ +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.core.util.AutoTypeConverter; +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(AutoTypeConverter.autoConvert(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/cool-admin-java/src/main/java/com/cool/modules/task/run/ScheduleRunnable.java b/cool-admin-java/src/main/java/com/cool/modules/task/run/ScheduleRunnable.java new file mode 100644 index 0000000..dbae43c --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/task/service/TaskInfoLogService.java b/cool-admin-java/src/main/java/com/cool/modules/task/service/TaskInfoLogService.java new file mode 100644 index 0000000..435c0a4 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/task/service/TaskInfoService.java b/cool-admin-java/src/main/java/com/cool/modules/task/service/TaskInfoService.java new file mode 100644 index 0000000..6f9657c --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/task/service/impl/TaskInfoLogServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/task/service/impl/TaskInfoLogServiceImpl.java new file mode 100644 index 0000000..d1f6ccd --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/task/service/impl/TaskInfoServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/task/service/impl/TaskInfoServiceImpl.java new file mode 100644 index 0000000..2d603d9 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/task/service/impl/TaskInfoServiceImpl.java @@ -0,0 +1,139 @@ +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.DETAIL, + TASK_LOG_ENTITY.STATUS, TASK_LOG_ENTITY.CREATE_TIME, + TASK_INFO_ENTITY.NAME).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.paginateAs(page, queryWrapper, TaskLogEntity.class); + } + + @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/cool-admin-java/src/main/java/com/cool/modules/task/utils/ScheduleUtils.java b/cool-admin-java/src/main/java/com/cool/modules/task/utils/ScheduleUtils.java new file mode 100644 index 0000000..7e82df5 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/user/controller/admin/AdminUserInfoController.java b/cool-admin-java/src/main/java/com/cool/modules/user/controller/admin/AdminUserInfoController.java new file mode 100644 index 0000000..833762d --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/AppUserAddressController.java b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/AppUserAddressController.java new file mode 100644 index 0000000..78aacc3 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/AppUserAddressController.java @@ -0,0 +1,49 @@ +package com.cool.modules.user.controller.app; + +import static com.cool.modules.user.entity.table.UserAddressEntityTableDef.USER_ADDRESS_ENTITY; + +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.core.util.CoolSecurityUtil; +import com.cool.modules.user.entity.UserAddressEntity; +import com.cool.modules.user.service.UserAddressService; +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 org.springframework.web.bind.annotation.GetMapping; + +/** + * 用户模块-收货地址 + */ +@Tag(name = "用户模块-收货地址", description = "用户模块-收货地址") +@CoolRestController(api = {"add", "delete", "update", "page", "list", "info"}) +public class AppUserAddressController extends BaseController { + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + setPageOption( + createOp() + .queryWrapper( + QueryWrapper.create() + .and(USER_ADDRESS_ENTITY.USER_ID.eq(CoolSecurityUtil.getCurrentUserId())) + .orderBy( + USER_ADDRESS_ENTITY.IS_DEFAULT.getName(), false))); + + setListOption( + createOp() + .queryWrapper( + QueryWrapper.create() + .and(USER_ADDRESS_ENTITY.USER_ID.eq(CoolSecurityUtil.getCurrentUserId())) + .orderBy( + USER_ADDRESS_ENTITY.IS_DEFAULT.getName(), false))); + } + + @Operation(summary = "默认地址", description = "默认地址") + @GetMapping("/default") + public R getDefault() { + Long userId = CoolSecurityUtil.getCurrentUserId(); + return R.ok(this.service.getDefault(userId)); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/AppUserInfoController.java b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/AppUserInfoController.java new file mode 100644 index 0000000..0f09313 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/AppUserInfoController.java @@ -0,0 +1,70 @@ +package com.cool.modules.user.controller.app; + +import cn.hutool.json.JSONObject; +import com.cool.core.annotation.CoolRestController; +import com.cool.core.request.R; +import com.cool.core.util.CoolSecurityUtil; +import com.cool.core.util.EntityUtils; +import com.cool.modules.user.entity.UserInfoEntity; +import com.cool.modules.user.service.UserInfoService; +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; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; + +@RequiredArgsConstructor +@Tag(name = "用户信息", description = "用户信息") +@CoolRestController +public class AppUserInfoController { + + private final UserInfoService userInfoService; + + @Operation(summary = "用户个人信息", description = "获得App、小程序或者其他应用的用户个人信息") + @GetMapping("/person") + public R person() { + Long userId = CoolSecurityUtil.getCurrentUserId(); + UserInfoEntity userInfoEntity = userInfoService.person(userId); + return R.ok(EntityUtils.toMap(userInfoEntity, + "password")); + } + + @Operation(summary = "更新用户信息") + @PostMapping("/updatePerson") + public R updatePerson(@RequestAttribute JSONObject requestParams) { + UserInfoEntity infoEntity = requestParams.toBean(UserInfoEntity.class); + infoEntity.setId(CoolSecurityUtil.getCurrentUserId()); + return R.ok( + userInfoService.updateById(infoEntity) + ); + } + + @Operation(summary = "更新用户密码") + @PostMapping("/updatePassword") + public R updatePassword( + @RequestAttribute JSONObject requestParams + ) { + String password = requestParams.get("password", String.class); + String code = requestParams.get("code", String.class); + userInfoService.updatePassword(CoolSecurityUtil.getCurrentUserId(), password, code); + return R.ok(); + } + + @Operation(summary = "注销") + @PostMapping("/logoff") + public R logoff() { + userInfoService.logoff(CoolSecurityUtil.getCurrentUserId()); + return R.ok(); + } + + @Operation(summary = "绑定手机号") + @PostMapping("/bindPhone") + public R bindPhone( + @RequestAttribute JSONObject requestParams) { + String phone = requestParams.get("phone", String.class); + String code = requestParams.get("code", String.class); + userInfoService.bindPhone(CoolSecurityUtil.getCurrentUserId(), phone, code); + return R.ok(); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/AppUserLoginController.java b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/AppUserLoginController.java new file mode 100644 index 0000000..53fe9d8 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/AppUserLoginController.java @@ -0,0 +1,182 @@ +package com.cool.modules.user.controller.app; + +import com.cool.core.annotation.CoolRestController; +import com.cool.core.annotation.TokenIgnore; +import com.cool.core.enums.UserTypeEnum; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.request.R; +import com.cool.modules.base.service.sys.BaseSysLoginService; +import com.cool.modules.user.controller.app.params.CaptchaParam; +import com.cool.modules.user.controller.app.params.LoginParam; +import com.cool.modules.user.controller.app.params.RefreshTokenParam; +import com.cool.modules.user.controller.app.params.SmsCodeParam; +import com.cool.modules.user.service.UserLoginService; +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; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@RequiredArgsConstructor +@Tag(name = "用户登录", description = "用户登录") +@CoolRestController +public class AppUserLoginController { + + private final UserLoginService userLoginService; + + private final BaseSysLoginService baseSysLoginService; + + /** + * 小程序登录 + */ + @TokenIgnore + @Operation(summary = "小程序登录") + @PostMapping("/mini") + public R mini(@RequestBody LoginParam param) { + String code = param.getCode(); + String encryptedData = param.getEncryptedData(); + String iv = param.getIv(); + CoolPreconditions.checkEmpty(code); + CoolPreconditions.checkEmpty(encryptedData); + CoolPreconditions.checkEmpty(iv); + return R.ok(userLoginService.mini(code, encryptedData, iv)); + } + + /** + * 公众号登录 + */ + @TokenIgnore + @Operation(summary = "公众号登录") + @PostMapping("/mp") + public R mp(@RequestBody LoginParam param) { + String code = param.getCode(); + CoolPreconditions.checkEmpty(code); + return R.ok(userLoginService.mp(code)); + } + + /** + * 微信APP授权登录 + */ + @TokenIgnore + @Operation(summary = "微信APP授权登录") + @PostMapping("/wxApp") + public R wxApp(@RequestBody LoginParam param) { + String code = param.getCode(); + CoolPreconditions.checkEmpty(code); + return R.ok(userLoginService.wxApp(code)); + } + + /** + * 手机号登录 + */ + @TokenIgnore + @Operation(summary = "手机号登录") + @PostMapping("/phone") + public R phone( + @RequestBody LoginParam param) { + String phone = param.getPhone(); + String smsCode = param.getSmsCode(); + CoolPreconditions.checkEmpty(phone); + CoolPreconditions.checkEmpty(smsCode); + return R.ok(userLoginService.phoneVerifyCode(phone, smsCode)); + } + + /** + * 一键手机号登录 + */ + @TokenIgnore + @Operation(summary = "一键手机号登录") + @PostMapping("/uniPhone") + public R uniPhone( + @RequestBody LoginParam param) { + String accessToken = param.getAccess_token(); + String openid = param.getOpenid(); + String appId = param.getAppId(); + CoolPreconditions.checkEmpty(accessToken); + CoolPreconditions.checkEmpty(openid); + CoolPreconditions.checkEmpty(appId); + return R.ok(userLoginService.uniPhone(accessToken, openid, appId)); + } + + /** + * 绑定小程序手机号 + */ + @TokenIgnore + @Operation(summary = "绑定小程序手机号") + @PostMapping("/miniPhone") + public R miniPhone(@RequestBody LoginParam param) { + String code = param.getCode(); + String encryptedData = param.getEncryptedData(); + String iv = param.getIv(); + CoolPreconditions.checkEmpty(code); + CoolPreconditions.checkEmpty(encryptedData); + CoolPreconditions.checkEmpty(iv); + return R.ok(userLoginService.miniPhone(code, encryptedData, iv)); + } + + /** + * 图片验证码 + */ + @TokenIgnore + @Operation(summary = "图片验证码") + @GetMapping("/captcha") + public R captcha( + @ModelAttribute CaptchaParam param) { + String type = param.getType(); + Integer width = param.getWidth(); + Integer height = param.getHeight(); + + CoolPreconditions.checkEmpty(type); + CoolPreconditions.checkEmpty(width); + CoolPreconditions.checkEmpty(height); + + return R.ok(baseSysLoginService.captcha(UserTypeEnum.APP, type, width, height)); + } + + /** + * 验证码 + */ + @TokenIgnore + @Operation(summary = "验证码") + @PostMapping("/smsCode") + public R smsCode( + @RequestBody SmsCodeParam param) { + String phone = param.getPhone(); + String captchaId = param.getCaptchaId(); + String code = param.getCode(); + + CoolPreconditions.checkEmpty(phone); + CoolPreconditions.checkEmpty(captchaId); + CoolPreconditions.checkEmpty(code); + userLoginService.smsCode(phone, captchaId, code); + return R.ok(); + } + + /** + * 刷新token + */ + @TokenIgnore + @Operation(summary = "刷新token") + @PostMapping("/refreshToken") + public R refreshToken(@RequestBody RefreshTokenParam param) { + return R.ok(userLoginService.refreshToken(param.getRefreshToken())); + } + + /** + * 密码登录 + */ + @TokenIgnore + @Operation(summary = "密码登录") + @PostMapping("/password") + public R password( + @RequestBody LoginParam param) { + String phone = param.getPhone(); + String password = param.getPassword(); + + CoolPreconditions.checkEmpty(phone); + CoolPreconditions.checkEmpty(password); + return R.ok(userLoginService.password(phone, password)); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/CaptchaParam.java b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/CaptchaParam.java new file mode 100644 index 0000000..b01495c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/CaptchaParam.java @@ -0,0 +1,12 @@ +package com.cool.modules.user.controller.app.params; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class CaptchaParam { + private String type; + private Integer width = 150; + private Integer height = 50; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/LoginParam.java b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/LoginParam.java new file mode 100644 index 0000000..bb4df32 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/LoginParam.java @@ -0,0 +1,34 @@ +package com.cool.modules.user.controller.app.params; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class LoginParam { + + /*******小程序/公众号/微信APP授权 登录*******/ + private String code; + + private String encryptedData; + + private String iv; + + + /*******手机号登录*******/ + private String phone; + + private String smsCode; + + + /*******一键手机号登录*******/ + private String access_token; + + private String openid; + + private String appId; + + + /*******密码登录*******/ + private String password; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/RefreshTokenParam.java b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/RefreshTokenParam.java new file mode 100644 index 0000000..c06065a --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/RefreshTokenParam.java @@ -0,0 +1,13 @@ +package com.cool.modules.user.controller.app.params; + +import lombok.Getter; +import lombok.Setter; + +/** + * 刷新token + */ +@Setter +@Getter +public class RefreshTokenParam { + private String refreshToken; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/SmsCodeParam.java b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/SmsCodeParam.java new file mode 100644 index 0000000..c5c8c75 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/controller/app/params/SmsCodeParam.java @@ -0,0 +1,12 @@ +package com.cool.modules.user.controller.app.params; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class SmsCodeParam { + private String phone; + private String captchaId; + private String code; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/entity/UserAddressEntity.java b/cool-admin-java/src/main/java/com/cool/modules/user/entity/UserAddressEntity.java new file mode 100644 index 0000000..391940e --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/entity/UserAddressEntity.java @@ -0,0 +1,43 @@ +package com.cool.modules.user.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; + +/** + * 用户模块-收货地址 + */ +@Getter +@Setter +@Table(value = "user_address", comment = "用户模块-收货地址") +public class UserAddressEntity extends BaseEntity { + + @Index + @ColumnDefine(comment = "用户ID", notNull = true) + private Long userId; + + @ColumnDefine(comment = "联系人", notNull = true) + private String contact; + + @Index + @ColumnDefine(comment = "手机号", length = 11, notNull = true) + private String phone; + + @ColumnDefine(comment = "省", notNull = true) + private String province; + + @ColumnDefine(comment = "市", notNull = true) + private String city; + + @ColumnDefine(comment = "区", notNull = true) + private String district; + + @ColumnDefine(comment = "地址", notNull = true) + private String address; + + @ColumnDefine(comment = "是否默认", defaultValue = "false") + private Boolean isDefault; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/entity/UserInfoEntity.java b/cool-admin-java/src/main/java/com/cool/modules/user/entity/UserInfoEntity.java new file mode 100644 index 0000000..d6914a9 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/entity/UserInfoEntity.java @@ -0,0 +1,54 @@ +package com.cool.modules.user.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import com.tangzc.mybatisflex.autotable.annotation.UniIndex; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Table(value = "user_info", comment = "用户信息") +public class UserInfoEntity extends BaseEntity { + + @UniIndex + @ColumnDefine(comment = "登录唯一ID") + private String unionid; + + @ColumnDefine(comment = "头像") + private String avatarUrl; + + @ColumnDefine(comment = "昵称") + private String nickName; + + @UniIndex + @ColumnDefine(comment = "手机号") + private String phone; + + @ColumnDefine(comment = "性别 0-未知 1-男 2-女", defaultValue = "0") + private Integer gender; + + @ColumnDefine(comment = "状态 0-禁用 1-正常 2-已注销", defaultValue = "1") + private Integer status; + + @ColumnDefine(comment = "登录方式 0-小程序 1-公众号 2-H5", defaultValue = "0") + private String loginType; + + @ColumnDefine(comment = "密码") + private String password; + + // 钉钉同步相关字段 + @UniIndex + @ColumnDefine(comment = "钉钉用户ID", length = 100) + private String dingtalkUserId; + + @ColumnDefine(comment = "钉钉部门ID") + private Long dingtalkDeptId; + + @ColumnDefine(comment = "工号", length = 50) + private String jobNumber; + + @ColumnDefine(comment = "职位", length = 100) + private String position; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/entity/UserWxEntity.java b/cool-admin-java/src/main/java/com/cool/modules/user/entity/UserWxEntity.java new file mode 100644 index 0000000..fe19040 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/entity/UserWxEntity.java @@ -0,0 +1,47 @@ +package com.cool.modules.user.entity; + +import com.cool.core.base.BaseEntity; +import com.mybatisflex.annotation.Table; +import com.cool.core.annotation.ColumnDefine; +import com.tangzc.mybatisflex.autotable.annotation.UniIndex; +import lombok.Getter; +import lombok.Setter; +import org.dromara.autotable.annotation.Index; + +@Getter +@Setter +@Table(value = "user_wx", comment = "微信用户") +public class UserWxEntity extends BaseEntity { + + @Index + @ColumnDefine(comment = "微信unionid") + private String unionid; + + @UniIndex + @ColumnDefine(comment = "微信openid", notNull = true) + private String openid; + + @ColumnDefine(comment = "头像") + private String avatarUrl; + + @ColumnDefine(comment = "昵称") + private String nickName; + + @ColumnDefine(comment = "性别 0-未知 1-男 2-女", defaultValue = "0") + private Integer gender; + + @ColumnDefine(comment = "语言") + private String language; + + @ColumnDefine(comment = "城市") + private String city; + + @ColumnDefine(comment = "省份") + private String province; + + @ColumnDefine(comment = "国家") + private String country; + + @ColumnDefine(comment = "类型 0-小程序 1-公众号 2-H5 3-APP", defaultValue = "0") + private Integer type; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/mapper/UserAddressMapper.java b/cool-admin-java/src/main/java/com/cool/modules/user/mapper/UserAddressMapper.java new file mode 100644 index 0000000..94ede5c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/mapper/UserAddressMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.user.mapper; + +import com.cool.modules.user.entity.UserAddressEntity; +import com.mybatisflex.core.BaseMapper; + +/** + * 用户模块-收货地址 + */ +public interface UserAddressMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/mapper/UserInfoMapper.java b/cool-admin-java/src/main/java/com/cool/modules/user/mapper/UserInfoMapper.java new file mode 100644 index 0000000..5b9f71d --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/java/com/cool/modules/user/mapper/UserWxMapper.java b/cool-admin-java/src/main/java/com/cool/modules/user/mapper/UserWxMapper.java new file mode 100644 index 0000000..f23a071 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/mapper/UserWxMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.user.mapper; + +import com.mybatisflex.core.BaseMapper; +import com.cool.modules.user.entity.UserWxEntity; + +/** + * 微信用户 + */ +public interface UserWxMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/proxy/WxProxy.java b/cool-admin-java/src/main/java/com/cool/modules/user/proxy/WxProxy.java new file mode 100644 index 0000000..ffcba4c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/proxy/WxProxy.java @@ -0,0 +1,36 @@ +package com.cool.modules.user.proxy; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.binarywang.wx.miniapp.bean.WxMaUserInfo; +import com.cool.core.util.CoolPluginInvokers; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import org.springframework.stereotype.Service; + +@Service +public class WxProxy { + public WxMaService getWxMaService() { + return (WxMaService)CoolPluginInvokers.invoke("wx", "getWxMaService"); + } + public WxMpService getWxMpService() { + return (WxMpService)CoolPluginInvokers.invoke("wx", "getWxMpService"); + } + + public WxMaJscode2SessionResult getSessionInfo(String jsCode) throws WxErrorException { + return getWxMaService().getUserService() + .getSessionInfo(jsCode); + } + + public WxMaPhoneNumberInfo getPhoneNumber(String jsCode) throws WxErrorException { + return getWxMaService().getUserService() + .getPhoneNumber(jsCode); + } + + public WxMaUserInfo getUserInfo(String sessionKey, String encryptedData, String ivStr) throws WxErrorException { + return getWxMaService().getUserService() + .getUserInfo(sessionKey, encryptedData, ivStr); + } + +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/service/UserAddressService.java b/cool-admin-java/src/main/java/com/cool/modules/user/service/UserAddressService.java new file mode 100644 index 0000000..da73041 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/service/UserAddressService.java @@ -0,0 +1,15 @@ +package com.cool.modules.user.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.user.entity.UserAddressEntity; + +/** + * 用户模块-收货地址 + */ +public interface UserAddressService extends BaseService { + + /** + * 获取默认地址 + */ + Object getDefault(Long userId); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/service/UserInfoService.java b/cool-admin-java/src/main/java/com/cool/modules/user/service/UserInfoService.java new file mode 100644 index 0000000..a779e80 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/service/UserInfoService.java @@ -0,0 +1,28 @@ +package com.cool.modules.user.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.user.entity.UserInfoEntity; + +public interface UserInfoService extends BaseService { + /** + * 用户个人信息 + * @param userId + * @return + */ + UserInfoEntity person(Long userId); + + /** + * 更新用户密码 + */ + void updatePassword(Long userId, String password, String code); + + /** + * 注销 + */ + void logoff(Long currentUserId); + + /** + * 绑定手机号 + */ + void bindPhone(Long currentUserId, String phone, String code); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/service/UserLoginService.java b/cool-admin-java/src/main/java/com/cool/modules/user/service/UserLoginService.java new file mode 100644 index 0000000..9c8048f --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/service/UserLoginService.java @@ -0,0 +1,57 @@ +package com.cool.modules.user.service; + +/** + * 用户登录 + */ +public interface UserLoginService { + + /** + * 发送短信验证码 + * @param phone + * @param captchaId + * @param code + */ + void smsCode(String phone, String captchaId, String code); + + /** + * 手机号验证码登录 + * @param phone + * @param smsCode + */ + Object phoneVerifyCode(String phone, String smsCode); + + + /** + * 刷新token + * + * @param refreshToken 刷新token + * @return 新的token + */ + Object refreshToken(String refreshToken); + /** + * 小程序登录 + */ + Object mini(String code, String encryptedData, String iv); + /** + * 公众号登录 + */ + Object mp(String code); + /** + * 微信APP授权登录 + */ + Object wxApp(String code); + + /** + * 一键手机号登录 + */ + Object uniPhone(String accessToken, String openid, String appId); + /** + * 绑定小程序手机号 + */ + Object miniPhone(String code, String encryptedData, String iv); + + /** + * 密码登录 + */ + Object password(String phone, String password); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/service/UserWxService.java b/cool-admin-java/src/main/java/com/cool/modules/user/service/UserWxService.java new file mode 100644 index 0000000..28e0d65 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/service/UserWxService.java @@ -0,0 +1,15 @@ +package com.cool.modules.user.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.user.entity.UserWxEntity; + +/** + * 微信用户 + */ +public interface UserWxService extends BaseService { + + /** + * 获取小程序用户信息 + */ + UserWxEntity getMiniUserInfo(String code, String encryptedData, String iv); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserAddressServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserAddressServiceImpl.java new file mode 100644 index 0000000..8ba4cfe --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserAddressServiceImpl.java @@ -0,0 +1,21 @@ +package com.cool.modules.user.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.user.entity.UserAddressEntity; +import com.cool.modules.user.mapper.UserAddressMapper; +import com.cool.modules.user.service.UserAddressService; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.stereotype.Service; + +/** + * 用户模块-收货地址 + */ +@Service +public class UserAddressServiceImpl extends BaseServiceImpl implements UserAddressService { + + @Override + public Object getDefault(Long userId) { + return this.getOne(QueryWrapper.create().eq(UserAddressEntity::getUserId, userId) + .eq(UserAddressEntity::getIsDefault, true).limit(1)); + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java new file mode 100644 index 0000000..e66e993 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserInfoServiceImpl.java @@ -0,0 +1,52 @@ +package com.cool.modules.user.service.impl; + +import cn.hutool.crypto.digest.MD5; +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 com.cool.modules.user.util.UserSmsUtil; +import com.cool.modules.user.util.UserSmsUtil.SendSceneEnum; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserInfoServiceImpl extends BaseServiceImpl implements + UserInfoService { + + private final UserSmsUtil userSmsUtil; + + @Override + public UserInfoEntity person(Long userId) { + UserInfoEntity info = getById(userId); + info.setPassword(null); + return info; + } + + @Override + public void updatePassword(Long userId, String password, String code) { + UserInfoEntity info = getById(userId); + userSmsUtil.checkVerifyCode(info.getPhone(), code, SendSceneEnum.ALL); + info.setPassword(MD5.create().digestHex(password)); + info.updateById(); + } + + @Override + public void logoff(Long userId) { + UserInfoEntity info = new UserInfoEntity(); + info.setId(userId); + info.setStatus(2); + info.setNickName("已注销-00" + userId); + info.updateById(); + } + + @Override + public void bindPhone(Long userId, String phone, String code) { + userSmsUtil.checkVerifyCode(phone, code, SendSceneEnum.ALL); + UserInfoEntity info = new UserInfoEntity(); + info.setId(userId); + info.setPhone(phone); + info.updateById(); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserLoginServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserLoginServiceImpl.java new file mode 100644 index 0000000..d8202a6 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserLoginServiceImpl.java @@ -0,0 +1,192 @@ +package com.cool.modules.user.service.impl; + +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.crypto.digest.MD5; +import cn.hutool.jwt.JWT; +import com.cool.core.cache.CoolCache; +import com.cool.core.enums.UserTypeEnum; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.security.jwt.JwtTokenUtil; +import com.cool.core.security.jwt.JwtUser; +import com.cool.modules.base.service.sys.BaseSysLoginService; +import com.cool.modules.user.entity.UserInfoEntity; +import com.cool.modules.user.entity.UserWxEntity; +import com.cool.modules.user.proxy.WxProxy; +import com.cool.modules.user.service.UserInfoService; +import com.cool.modules.user.service.UserLoginService; +import com.cool.modules.user.service.UserWxService; +import com.cool.modules.user.util.UserSmsUtil; +import com.cool.modules.user.util.UserSmsUtil.SendSceneEnum; +import com.mybatisflex.core.query.QueryWrapper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class UserLoginServiceImpl implements UserLoginService { + + private final CoolCache coolCache; + + private final JwtTokenUtil jwtTokenUtil; + + private final UserInfoService userInfoService; + + private final UserSmsUtil userSmsUtil; + + private final BaseSysLoginService baseSysLoginService; + + private final UserWxService userWxService; + + private final WxProxy wxProxy; + private final static List authority = + List.of(new SimpleGrantedAuthority("ROLE_" + UserTypeEnum.APP.name())); + + @Override + public void smsCode(String phone, String captchaId, String code) { + // 校验图片验证码,不通过直接抛异常 + baseSysLoginService.captchaCheck(captchaId, code); + userSmsUtil.sendVerifyCode(phone, SendSceneEnum.ALL); + coolCache.del("verify:img:" + captchaId); + } + + @Override + public Object phoneVerifyCode(String phone, String smsCode) { + // 校验短信验证码,不通过直接抛异常 + userSmsUtil.checkVerifyCode(phone, smsCode, SendSceneEnum.ALL); + return generateTokenByPhone(phone); + } + + + @Override + public Object refreshToken(String refreshToken) { + CoolPreconditions.check(!jwtTokenUtil.validateRefreshToken(refreshToken), "错误的refreshToken"); + JWT jwt = jwtTokenUtil.getTokenInfo(refreshToken); + CoolPreconditions.check(jwt == null || !(Boolean) jwt.getPayload("isRefresh"), + "错误的refreshToken"); + Long userId = Convert.toLong(jwt.getPayload("userId")); + return generateToken(userId, refreshToken); + } + + @Override + public Object mini(String code, String encryptedData, String iv) { + UserWxEntity userWxEntity = userWxService.getMiniUserInfo(code, encryptedData, iv); + return wxLoginToken(userWxEntity); + } + + private Object wxLoginToken(UserWxEntity userWxEntity) { + String unionId = ObjUtil.isNotEmpty(userWxEntity.getUnionid()) ? userWxEntity.getUnionid() + : userWxEntity.getOpenid(); + UserInfoEntity userInfoEntity = userInfoService.getOne( + QueryWrapper.create().eq(UserInfoEntity::getUnionid, unionId)); + if (ObjUtil.isEmpty(userInfoEntity)) { + userInfoEntity = new UserInfoEntity(); + userInfoEntity.setNickName(ObjUtil.isNotEmpty(userWxEntity.getNickName()) ? userWxEntity.getNickName() : generateRandomNickname()); + userInfoEntity.setGender(userWxEntity.getGender()); + userInfoEntity.setAvatarUrl(userWxEntity.getAvatarUrl()); + userInfoEntity.setUnionid(unionId); + userInfoEntity.save(); + } + return generateToken(userInfoEntity, null); + } + + @Override + public Object mp(String code) { + return null; + } + + @Override + public Object wxApp(String code) { + return null; + } + + @Override + public Object uniPhone(String accessToken, String openid, String appId) { + return null; + } + + @Override + public Object miniPhone(String code, String encryptedData, String iv) { + try { + WxMaPhoneNumberInfo phoneNumber = wxProxy.getPhoneNumber(code); + CoolPreconditions.checkEmpty(phoneNumber, "微信登录失败"); + return generateTokenByPhone(phoneNumber.getPhoneNumber()); + } catch (WxErrorException e) { + CoolPreconditions.alwaysThrow(e.getMessage(), e); + } + CoolPreconditions.alwaysThrow("微信登录失败"); + return null; + } + + @Override + public Object password(String phone, String password) { + UserInfoEntity userInfoEntity = userInfoService.getOne( + QueryWrapper.create().eq(UserInfoEntity::getPhone, phone)); + CoolPreconditions.checkEmpty(userInfoEntity, "账号或密码错误"); + if (userInfoEntity.getPassword().equals(MD5.create().digestHex(password))) { + return generateToken(userInfoEntity, null); + } + CoolPreconditions.checkEmpty(userInfoEntity, "账号或密码错误"); + return null; + } + + /** + * 前置已校验用户的手机号, + * 根据手机号找到用户生成token + */ + private Object generateTokenByPhone(String phone) { + UserInfoEntity userInfoEntity = userInfoService.getOne( + QueryWrapper.create().eq(UserInfoEntity::getPhone, phone)); + if (ObjUtil.isEmpty(userInfoEntity)) { + userInfoEntity = new UserInfoEntity(); + userInfoEntity.setPhone(phone); + // 生成随机昵称 + userInfoEntity.setNickName(generateRandomNickname()); + userInfoEntity.save(); + } + return generateToken(userInfoEntity, null); + } + + /** + * + * @return 生成的昵称 + */ + private String generateRandomNickname() { + // 定义昵称的长度 + int length = 8; + // 生成随机字符串 + return RandomUtil.randomString(length); + } + /** + * 生成token + */ + private Dict generateToken(Long userId, String refreshToken) { + UserInfoEntity userInfoEntity = userInfoService.getById(userId); + return generateToken(userInfoEntity, refreshToken); + } + private Dict generateToken(UserInfoEntity userInfoEntity, String refreshToken) { + Dict tokenInfo = Dict.create() + .set("userType", UserTypeEnum.APP.name()) + .set("userId", userInfoEntity.getId()); + String token = jwtTokenUtil.generateToken(tokenInfo); + if (ObjUtil.isEmpty(refreshToken)) { + refreshToken = jwtTokenUtil.generateRefreshToken(tokenInfo); + } + JwtUser jwtUser = new JwtUser(userInfoEntity.getId(), + authority, + ObjUtil.equals(userInfoEntity.getStatus(), 1)); + coolCache.set("app:userDetails:" + jwtUser.getUserId(), jwtUser); + return Dict.create() + .set("token", token) + .set("expire", jwtTokenUtil.getExpire()) + .set("refreshToken", refreshToken) + .set("refreshExpire", jwtTokenUtil.getRefreshExpire()); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserWxServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserWxServiceImpl.java new file mode 100644 index 0000000..6b8286c --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/service/impl/UserWxServiceImpl.java @@ -0,0 +1,60 @@ +package com.cool.modules.user.service.impl; + +import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; +import cn.binarywang.wx.miniapp.bean.WxMaUserInfo; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjUtil; +import com.cool.core.base.BaseServiceImpl; +import com.cool.core.exception.CoolPreconditions; +import com.cool.modules.user.entity.UserWxEntity; +import com.cool.modules.user.mapper.UserWxMapper; +import com.cool.modules.user.proxy.WxProxy; +import com.cool.modules.user.service.UserWxService; +import com.mybatisflex.core.query.QueryWrapper; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import org.springframework.stereotype.Service; + +/** + * 微信用户 + */ +@Service +@RequiredArgsConstructor +public class UserWxServiceImpl extends BaseServiceImpl implements UserWxService { + + private final WxProxy wxProxy; + + /** + * 获得小程序用户信息 + */ + public UserWxEntity getMiniUserInfo(String code, String encryptedData, String iv) { + // 获取 session + WxMaJscode2SessionResult result = null; + try { + result = wxProxy.getSessionInfo(code); + // 解密数据 + WxMaUserInfo wxMaUserInfo = wxProxy.getUserInfo(result.getSessionKey(), encryptedData, iv); + if (ObjUtil.isNotEmpty(wxMaUserInfo)) { + UserWxEntity userWxEntity = BeanUtil.copyProperties(wxMaUserInfo, UserWxEntity.class); + userWxEntity.setOpenid(result.getOpenid()); + userWxEntity.setUnionid(wxMaUserInfo.getUnionId()); + return getBySave(userWxEntity, 0); + } + } catch (WxErrorException e) { + CoolPreconditions.alwaysThrow(e.getMessage(), e); + } + CoolPreconditions.alwaysThrow("获得小程序用户信息"); + return null; + } + + public UserWxEntity getBySave(UserWxEntity entity, int type) { + UserWxEntity one = this.getOne( + QueryWrapper.create().eq(UserWxEntity::getOpenid, entity.getOpenid())); + if (ObjUtil.isEmpty(one)) { + entity.setType(type); + super.save(entity); + return entity; + } + return one; + } +} \ No newline at end of file diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/util/UserSmsUtil.java b/cool-admin-java/src/main/java/com/cool/modules/user/util/UserSmsUtil.java new file mode 100644 index 0000000..d0d81e2 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/util/UserSmsUtil.java @@ -0,0 +1,96 @@ +package com.cool.modules.user.util; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.cool.core.cache.CoolCache; +import com.cool.core.exception.CoolPreconditions; +import com.cool.core.plugin.service.CoolPluginService; +import com.cool.core.util.CoolPluginInvokers; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * UserSmsUtil - 用户短信工具类 + * 该类用于发送短信验证码。 + */ +@RequiredArgsConstructor +@Slf4j +@Component +public class UserSmsUtil { + + /** + * 短信发送场景枚举 + */ + public enum SendSceneEnum { + ALL, + } + + private final CoolPluginService coolPluginService; + + private final CoolCache coolCache; + + /** + * 发送短信验证码 + * + * @param phone + */ + public void sendVerifyCode(String phone, SendSceneEnum sendSceneEnum) { + // 随机生成4位验证码 + String verifyCode = RandomUtil.randomNumbers(4); + send(phone, verifyCode); + coolCache.set(sendSceneEnum.name() + "_sms:" + phone, verifyCode, 60 * 10); + } + + /** + * 检查验证码 + * @param phone + * @param code + * @return + */ + public void checkVerifyCode(String phone, String code, SendSceneEnum sendSceneEnum) { + // 万能验证码(测试环境) + if ("6666".equals(code)) { + log.info("使用万能验证码登录, phone: {}", phone); + return; + } + + String key = sendSceneEnum.name() + "_sms:" + phone; + String cacheCode = coolCache.get(key, String.class); + boolean flag = StrUtil.isNotEmpty(code) && code.equals(cacheCode); + if (flag) { + // 删除验证码 + coolCache.del(key); + } + CoolPreconditions.check(!flag, "验证码错误"); + } + + /** + * 发送短信 + * + * @param phone + * @param code + */ + public void send(String phone, String code) { + List phones = new ArrayList<>(); + phones.add(phone); + + Map params = new HashMap<>(); + params.put("code", code); + // 插件key sms-tx、sms-ali,哪个实例存在就调用哪个 + if (coolPluginService.getInstanceWithoutCheck("sms-tx") != null) { + // 调用腾讯短信插件 + CoolPluginInvokers.invoke("sms-tx", "send", phones, params); + } else if (coolPluginService.getInstanceWithoutCheck("sms-ali") != null) { + // 调用阿里短信插件 + CoolPluginInvokers.invoke("sms-ali", "send", phones, params); + } else { + // 未找到短信插件 + log.error("未找到短信插件,请前往插件市场下载安装"); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/user/util/UserWxUtil.java b/cool-admin-java/src/main/java/com/cool/modules/user/util/UserWxUtil.java new file mode 100644 index 0000000..74b32eb --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/user/util/UserWxUtil.java @@ -0,0 +1,12 @@ +package com.cool.modules.user.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * UserWxUtil - 用户微信工具类 + */ +@Component +@RequiredArgsConstructor +public class UserWxUtil { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/useradvice/controller/app/AppUserAdviceController.java b/cool-admin-java/src/main/java/com/cool/modules/useradvice/controller/app/AppUserAdviceController.java new file mode 100644 index 0000000..eff0103 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/useradvice/controller/app/AppUserAdviceController.java @@ -0,0 +1,192 @@ +package com.cool.modules.useradvice.controller.app; + +import cn.hutool.json.JSONObject; +import com.cool.core.request.R; +import com.cool.modules.dailyreport.entity.DailyReportEntity; +import com.cool.modules.dailyreport.service.DailyReportAiService; +import com.cool.modules.dailyreport.service.DailyReportService; +import com.cool.modules.monthlyreport.entity.MonthlyReportEntity; +import com.cool.modules.monthlyreport.service.MonthlyReportService; +import com.cool.modules.weeklyreport.entity.WeeklyReportEntity; +import com.cool.modules.weeklyreport.service.WeeklyReportService; +import com.cool.modules.base.service.sys.BaseSysParamService; +import com.mybatisflex.core.query.QueryWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.Comparator; +import java.util.List; + +/** + * 用户端工作建议(周/月) + */ +@Slf4j +@Tag(name = "用户端工作建议", description = "基于周/月聚合内容生成个人建议") +@RestController +@RequestMapping("/app/useradvice") +@RequiredArgsConstructor +public class AppUserAdviceController { + + private final WeeklyReportService weeklyReportService; + private final MonthlyReportService monthlyReportService; + private final DailyReportService dailyReportService; + private final DailyReportAiService dailyReportAiService; + private final BaseSysParamService baseSysParamService; + + @Operation(summary = "获取个人工作建议") + @GetMapping("/advice") + public R getAdvice( + @RequestParam Long userId, + @RequestParam String scope, // week | month + @RequestParam String startDate // 支持 yyyy-MM 或 yyyy-MM-dd(本地时区) + ) { + try { + String content = null; + LocalDate start; + LocalDate end; + + if ("week".equalsIgnoreCase(scope)) { + // 周:要求 yyyy-MM-dd;若传 yyyy-MM 则当作该月第一天 + if (startDate != null && startDate.matches("^\\d{4}-\\d{2}$")) { + YearMonth ym = YearMonth.parse(startDate); + start = ym.atDay(1); + } else { + start = LocalDate.parse(startDate); + } + end = start.plusDays(6); + + // 优先:当周已提交周报 + QueryWrapper wq = QueryWrapper.create() + .eq(WeeklyReportEntity::getUserId, userId) + .eq(WeeklyReportEntity::getWeekStartDate, start) + .eq(WeeklyReportEntity::getWeekEndDate, end) + .eq(WeeklyReportEntity::getStatus, 1); + WeeklyReportEntity weekly = weeklyReportService.getOne(wq); + if (weekly != null) { + content = pickContent(weekly.getUserEditedContent(), weekly.getAiFormattedContent(), weekly.getOriginalText()); + } + + // 回退:当周已提交日报聚合 + if (isBlank(content)) { + List list = dailyReportService.getUserReports(userId, start, end, 1); + StringBuilder sb = new StringBuilder(); + list.stream() + .sorted(Comparator.comparing(DailyReportEntity::getReportDate)) + .forEach(r -> { + String c = pickContent(r.getUserEditedContent(), r.getAiFormattedContent(), r.getOriginalText()); + if (!isBlank(c)) { + sb.append("- ").append(r.getReportDate()).append(": ") + .append(c).append("\n"); + } + }); + content = sb.toString(); + } + } else { + // month:支持 yyyy-MM 或 yyyy-MM-dd + YearMonth ym; + if (startDate != null && startDate.matches("^\\d{4}-\\d{2}$")) { + ym = YearMonth.parse(startDate); + } else { + start = LocalDate.parse(startDate); + ym = YearMonth.from(start); + } + LocalDate first = ym.atDay(1); + LocalDate last = ym.atEndOfMonth(); + start = first; + end = last; + + // 优先:当月已提交月报 + QueryWrapper mq = QueryWrapper.create() + .eq(MonthlyReportEntity::getUserId, userId) + .eq(MonthlyReportEntity::getMonth, ym.toString()) + .eq(MonthlyReportEntity::getStatus, 1); + MonthlyReportEntity monthly = monthlyReportService.getOne(mq); + if (monthly != null) { + content = pickContent(monthly.getUserEditedContent(), monthly.getAiFormattedContent(), monthly.getOriginalText()); + } + + // 回退1:当月范围内的已提交周报聚合 + if (isBlank(content)) { + QueryWrapper wq = QueryWrapper.create() + .eq(WeeklyReportEntity::getUserId, userId) + .ge(WeeklyReportEntity::getWeekStartDate, first) + .le(WeeklyReportEntity::getWeekEndDate, last) + .eq(WeeklyReportEntity::getStatus, 1); + List weeks = weeklyReportService.list(wq); + StringBuilder sb = new StringBuilder(); + weeks.stream() + .sorted(Comparator.comparing(WeeklyReportEntity::getWeekStartDate)) + .forEach(r -> { + String c = pickContent(r.getUserEditedContent(), r.getAiFormattedContent(), r.getOriginalText()); + if (!isBlank(c)) { + sb.append("- ").append(r.getWeekStartDate()).append(" ~ ") + .append(r.getWeekEndDate()).append(": ") + .append(c).append("\n"); + } + }); + content = sb.toString(); + } + + // 回退2:当月范围内的已提交日报聚合 + if (isBlank(content)) { + List list = dailyReportService.getUserReports(userId, first, last, 1); + StringBuilder sb = new StringBuilder(); + list.stream() + .sorted(Comparator.comparing(DailyReportEntity::getReportDate)) + .forEach(r -> { + String c = pickContent(r.getUserEditedContent(), r.getAiFormattedContent(), r.getOriginalText()); + if (!isBlank(c)) { + sb.append("- ").append(r.getReportDate()).append(": ") + .append(c).append("\n"); + } + }); + content = sb.toString(); + } + } + + if (isBlank(content)) { + String msg = "week".equalsIgnoreCase(scope) + ? "未查询到该周的可用内容,请核实后再尝试" + : "未查询到该月份的月报/周报/日报内容,请核实后再尝试"; + return R.error(msg); + } + + String paramKey = "week".equalsIgnoreCase(scope) + ? "ai.user.advice.weekly" : "ai.user.advice.monthly"; + String defaultKey = "week".equalsIgnoreCase(scope) + ? "user_advice_weekly" : "user_advice_monthly"; + String configuredKey = baseSysParamService.dataByKey(paramKey); + String templateKey = (configuredKey != null && !configuredKey.isBlank()) ? configuredKey : defaultKey; + + String advice = dailyReportAiService.generateAdvice(templateKey, content); + + JSONObject result = new JSONObject(); + result.set("scope", scope); + result.set("startDate", start.toString()); + result.set("endDate", end.toString()); + result.set("adviceText", advice); + return R.ok(result); + } catch (Exception e) { + log.error("【用户端个人建议】生成失败", e); + return R.error("生成失败: " + e.getMessage()); + } + } + + private static boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } + + private static String pickContent(String edited, String ai, String original) { + if (!isBlank(edited)) return edited; + if (!isBlank(ai)) return ai; + return original; + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/controller/admin/AdminWeeklyReportController.java b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/controller/admin/AdminWeeklyReportController.java new file mode 100644 index 0000000..eef8d40 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/controller/admin/AdminWeeklyReportController.java @@ -0,0 +1,148 @@ +package com.cool.modules.weeklyreport.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.weeklyreport.entity.WeeklyReportEntity; +import com.cool.modules.weeklyreport.service.WeeklyReportService; +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 lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.cool.modules.weeklyreport.entity.table.WeeklyReportEntityTableDef.WEEKLY_REPORT_ENTITY; +import com.cool.modules.user.entity.UserInfoEntity; +import com.cool.modules.user.mapper.UserInfoMapper; + +/** + * 管理端周报Controller + */ +@Slf4j +@Tag(name = "管理端周报管理", description = "管理端周报查询、统计等功能") +@CoolRestController(api = {"page", "list", "info"}) +@RequiredArgsConstructor +public class AdminWeeklyReportController extends BaseController { + + private final WeeklyReportService weeklyReportService; + private final UserInfoMapper userInfoMapper; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + QueryWrapper qw = QueryWrapper.create() + .orderBy(WEEKLY_REPORT_ENTITY.WEEK_START_DATE, false); + + setPageOption(createOp() + .keyWordLikeFields(WEEKLY_REPORT_ENTITY.USER_EDITED_CONTENT) + .fieldEq(WEEKLY_REPORT_ENTITY.STATUS) + // 移除默认的 USER_ID 精确筛选,避免未传参时隐式约束导致列表为空 + .queryWrapper(qw) + ); + } + + @Operation(summary = "获取周报提交统计") + @GetMapping("/statistics") + public R getStatistics( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + + log.info("【周报统计】开始统计周报提交情况, startDate={}, endDate={}", startDate, endDate); + + try { + QueryWrapper qw = QueryWrapper.create(); + + // 添加时间范围筛选 + if (startDate != null && !startDate.isEmpty()) { + qw.ge(WeeklyReportEntity::getWeekStartDate, LocalDate.parse(startDate)); + } + if (endDate != null && !endDate.isEmpty()) { + qw.le(WeeklyReportEntity::getWeekEndDate, LocalDate.parse(endDate)); + } + + // 总提交数 + long totalCount = weeklyReportService.count(qw); + + // 已提交数 + QueryWrapper submittedQw = qw.clone().eq(WeeklyReportEntity::getStatus, 1); + long submittedCount = weeklyReportService.count(submittedQw); + + // 草稿数 + QueryWrapper draftQw = qw.clone().eq(WeeklyReportEntity::getStatus, 0); + long draftCount = weeklyReportService.count(draftQw); + + JSONObject result = new JSONObject(); + result.set("totalCount", totalCount); + result.set("submittedCount", submittedCount); + result.set("draftCount", draftCount); + + log.info("【周报统计】统计成功, 总数={}, 已提交={}, 草稿={}", + totalCount, submittedCount, draftCount); + + return R.ok(result); + } catch (Exception e) { + log.error("【周报统计】统计失败", e); + return R.error("统计失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取未提交周报的人员列表", description = "根据周起始日统计未提交周报的人员") + @GetMapping("/unsubmitted") + public R getUnsubmittedUsers(@RequestParam(required = false) String weekStart) { + try { + LocalDate start; + if (weekStart == null || weekStart.isEmpty()) { + LocalDate today = LocalDate.now(); + start = today.minusDays((today.getDayOfWeek().getValue() + 6) % 7); + } else { + start = LocalDate.parse(weekStart); + } + LocalDate end = start.plusDays(6); + + QueryWrapper submittedQw = QueryWrapper.create() + .ge(WeeklyReportEntity::getWeekStartDate, start) + .le(WeeklyReportEntity::getWeekEndDate, end) + .eq(WeeklyReportEntity::getStatus, 1); + + List submitted = weeklyReportService.list(submittedQw); + Set submittedUserIds = submitted.stream() + .map(WeeklyReportEntity::getUserId) + .filter(id -> id != null) + .collect(Collectors.toSet()); + + List allUsers = userInfoMapper.selectListByQuery(QueryWrapper.create()); + List unsubmittedUsers = allUsers.stream() + .filter(u -> u.getId() != null && !submittedUserIds.contains(u.getId())) + .collect(Collectors.toList()); + + Map result = new HashMap<>(); + result.put("weekStart", start); + result.put("weekEnd", end); + result.put("unsubmittedCount", unsubmittedUsers.size()); + result.put("users", unsubmittedUsers); + + return R.ok(result); + } catch (Exception e) { + log.error("【周报统计】获取未提交周报人员失败", e); + return R.error("获取未提交人员失败: " + e.getMessage()); + } + } + + @Operation(summary = "导出周报(TODO)") + @GetMapping("/export") + public R export() { + log.info("【周报导出】导出功能待实现"); + // TODO: 实现导出功能 + return R.error("导出功能待实现"); + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/controller/app/AppWeeklyReportController.java b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/controller/app/AppWeeklyReportController.java new file mode 100644 index 0000000..897a95f --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/controller/app/AppWeeklyReportController.java @@ -0,0 +1,430 @@ +package com.cool.modules.weeklyreport.controller.app; + +import cn.hutool.json.JSONObject; +import com.cool.core.base.BaseController; +import com.cool.core.request.R; +import com.cool.modules.weeklyreport.entity.WeeklyReportEntity; +import com.cool.modules.weeklyreport.service.WeeklyReportAiService; +import com.cool.modules.weeklyreport.service.WeeklyReportService; +import com.cool.modules.dailyreport.entity.DailyReportEntity; +import com.cool.modules.dailyreport.service.DailyReportService; +import com.cool.modules.dailyreport.service.DailyReportAiService; +import com.mybatisflex.core.query.QueryWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import jakarta.servlet.http.HttpServletRequest; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Comparator; + +import static com.cool.modules.weeklyreport.entity.table.WeeklyReportEntityTableDef.WEEKLY_REPORT_ENTITY; + +/** + * 用户端周报Controller + */ +@Slf4j +@Tag(name = "用户端周报管理", description = "用户端周报提交、查询等功能") +@RestController +@RequestMapping("/app/weeklyreport/report") +@RequiredArgsConstructor +public class AppWeeklyReportController extends BaseController { + + private final WeeklyReportService weeklyReportService; + private final WeeklyReportAiService weeklyReportAiService; + private final DailyReportService dailyReportService; + private final DailyReportAiService dailyReportAiService; + + @Override + protected void init(HttpServletRequest request, JSONObject requestParams) { + // 应用端当前未使用通用分页/列表接口,此处无需配置 + } + + @Operation(summary = "提交周报") + @PostMapping("/submit") + public R submitWeeklyReport(@RequestBody JSONObject data) { + log.info("【周报提交】开始提交周报, 请求数据: {}", data); + + try { + Long userId = data.getLong("userId"); + LocalDate weekStartDate = LocalDate.parse(data.getStr("weekStartDate")); + LocalDate weekEndDate = LocalDate.parse(data.getStr("weekEndDate")); + String originalText = data.getStr("originalText"); + String aiFormattedContent = data.getStr("aiFormattedContent"); + String userEditedContent = data.getStr("userEditedContent"); + Integer inputType = data.getInt("inputType", 0); + + log.info("【周报提交】解析参数: userId={}, weekStartDate={}, weekEndDate={}", + userId, weekStartDate, weekEndDate); + + // 查询当前周是否已有周报 + QueryWrapper qw = QueryWrapper.create() + .eq(WeeklyReportEntity::getUserId, userId) + .eq(WeeklyReportEntity::getWeekStartDate, weekStartDate) + .eq(WeeklyReportEntity::getWeekEndDate, weekEndDate); + + WeeklyReportEntity existingReport = weeklyReportService.getOne(qw); + + WeeklyReportEntity report; + if (existingReport != null) { + // 更新已有周报 + log.info("【周报提交】当前周已有周报,执行更新操作, reportId={}", existingReport.getId()); + existingReport.setOriginalText(originalText); + existingReport.setAiFormattedContent(aiFormattedContent); + existingReport.setUserEditedContent(userEditedContent); + existingReport.setInputType(inputType); + existingReport.setStatus(1); // 已提交 + existingReport.setSubmitTime(LocalDateTime.now()); + weeklyReportService.updateById(existingReport); + report = existingReport; + } else { + // 新增周报 + log.info("【周报提交】当前周无周报,执行新增操作"); + report = new WeeklyReportEntity(); + report.setUserId(userId); + report.setWeekStartDate(weekStartDate); + report.setWeekEndDate(weekEndDate); + report.setOriginalText(originalText); + report.setAiFormattedContent(aiFormattedContent); + report.setUserEditedContent(userEditedContent); + report.setInputType(inputType); + report.setStatus(1); // 已提交 + report.setSubmitTime(LocalDateTime.now()); + weeklyReportService.save(report); + } + + log.info("【周报提交】周报提交成功, reportId={}", report.getId()); + return R.ok(report); + } catch (Exception e) { + log.error("【周报提交】周报提交失败", e); + return R.error("周报提交失败: " + e.getMessage()); + } + } + + @Operation(summary = "保存草稿") + @PostMapping("/saveDraft") + public R saveDraft(@RequestBody JSONObject data) { + log.info("【周报草稿】开始保存草稿, 请求数据: {}", data); + + try { + Long userId = data.getLong("userId"); + LocalDate weekStartDate = LocalDate.parse(data.getStr("weekStartDate")); + LocalDate weekEndDate = LocalDate.parse(data.getStr("weekEndDate")); + String originalText = data.getStr("originalText"); + String aiFormattedContent = data.getStr("aiFormattedContent"); + String userEditedContent = data.getStr("userEditedContent"); + Integer inputType = data.getInt("inputType", 0); + + // 查询当前周是否已有周报 + QueryWrapper qw = QueryWrapper.create() + .eq(WeeklyReportEntity::getUserId, userId) + .eq(WeeklyReportEntity::getWeekStartDate, weekStartDate) + .eq(WeeklyReportEntity::getWeekEndDate, weekEndDate); + + WeeklyReportEntity existingReport = weeklyReportService.getOne(qw); + + WeeklyReportEntity report; + if (existingReport != null) { + // 更新已有草稿 + log.info("【周报草稿】当前周已有周报,执行更新操作, reportId={}", existingReport.getId()); + existingReport.setOriginalText(originalText); + existingReport.setAiFormattedContent(aiFormattedContent); + existingReport.setUserEditedContent(userEditedContent); + existingReport.setInputType(inputType); + weeklyReportService.updateById(existingReport); + report = existingReport; + } else { + // 新增草稿 + log.info("【周报草稿】当前周无周报,执行新增操作"); + report = new WeeklyReportEntity(); + report.setUserId(userId); + report.setWeekStartDate(weekStartDate); + report.setWeekEndDate(weekEndDate); + report.setOriginalText(originalText); + report.setAiFormattedContent(aiFormattedContent); + report.setUserEditedContent(userEditedContent); + report.setInputType(inputType); + report.setStatus(0); // 草稿 + weeklyReportService.save(report); + } + + log.info("【周报草稿】草稿保存成功, reportId={}", report.getId()); + return R.ok(report); + } catch (Exception e) { + log.error("【周报草稿】草稿保存失败", e); + return R.error("草稿保存失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取我的周报列表") + @GetMapping("/myReports") + public R getMyWeeklyReports( + @RequestParam Long userId, + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate, + @RequestParam(required = false) Integer status, + @RequestParam(required = false, defaultValue = "1") Integer page, + @RequestParam(required = false, defaultValue = "20") Integer size) { + + log.info("【周报查询】开始查询我的周报列表, userId={}, startDate={}, endDate={}, status={}, page={}, size={}", + userId, startDate, endDate, status, page, size); + + try { + QueryWrapper qw = QueryWrapper.create() + .eq(WeeklyReportEntity::getUserId, userId) + .orderBy(WEEKLY_REPORT_ENTITY.WEEK_START_DATE, false); + + // 添加时间范围筛选 + if (startDate != null && !startDate.isEmpty()) { + qw.ge(WeeklyReportEntity::getWeekStartDate, LocalDate.parse(startDate)); + } + if (endDate != null && !endDate.isEmpty()) { + qw.le(WeeklyReportEntity::getWeekEndDate, LocalDate.parse(endDate)); + } + + // 添加状态筛选 + if (status != null) { + qw.eq(WeeklyReportEntity::getStatus, status); + } + + // 分页查询 + List reports = weeklyReportService.list(qw); + + log.info("【周报查询】查询成功, 数量: {}", reports.size()); + return R.ok(reports); + } catch (Exception e) { + log.error("【周报查询】查询失败", e); + return R.error("查询失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取周报详情") + @GetMapping("/detail") + public R getWeeklyReportDetail(@RequestParam Long id, @RequestParam Long userId) { + log.info("【周报详情】开始查询周报详情, id={}, userId={}", id, userId); + + try { + WeeklyReportEntity report = weeklyReportService.getById(id); + + if (report == null) { + log.warn("【周报详情】周报不存在, id={}", id); + return R.error("周报不存在"); + } + + // 验证权限 + if (!report.getUserId().equals(userId)) { + log.warn("【周报详情】无权限查看该周报, userId={}, reportUserId={}", userId, report.getUserId()); + return R.error("无权限查看该周报"); + } + + log.info("【周报详情】查询成功"); + return R.ok(report); + } catch (Exception e) { + log.error("【周报详情】查询失败", e); + return R.error("查询失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取当前周的周报") + @GetMapping("/currentWeekReport") + public R getCurrentWeekReport(@RequestParam Long userId) { + log.info("【当前周周报】开始查询当前周周报, userId={}", userId); + + try { + // 计算当前周的周一和周日 + LocalDate today = LocalDate.now(); + LocalDate weekStart = today.with(DayOfWeek.MONDAY); + LocalDate weekEnd = today.with(DayOfWeek.SUNDAY); + + log.info("【当前周周报】当前周范围: {} - {}", weekStart, weekEnd); + + QueryWrapper qw = QueryWrapper.create() + .eq(WeeklyReportEntity::getUserId, userId) + .eq(WeeklyReportEntity::getWeekStartDate, weekStart) + .eq(WeeklyReportEntity::getWeekEndDate, weekEnd); + + WeeklyReportEntity report = weeklyReportService.getOne(qw); + + if (report != null) { + log.info("【当前周周报】查询成功, reportId={}", report.getId()); + } else { + log.info("【当前周周报】当前周暂无周报"); + } + + return R.ok(report); + } catch (Exception e) { + log.error("【当前周周报】查询失败", e); + return R.error("查询失败: " + e.getMessage()); + } + } + + @Operation(summary = "AI格式化周报") + @PostMapping("/aiFormat") + public R aiFormat(@RequestBody JSONObject data) { + log.info("【周报AI格式化】开始AI格式化, 请求数据: {}", data); + + try { + String originalText = data.getStr("originalText"); + String templateKey = data.getStr("templateKey", "weekly_report_format"); + String weekStartDate = data.getStr("weekStartDate"); + String weekEndDate = data.getStr("weekEndDate"); + + log.info("【周报AI格式化】参数: originalText长度={}, templateKey={}, weekStartDate={}, weekEndDate={}", + originalText != null ? originalText.length() : 0, templateKey, weekStartDate, weekEndDate); + + String formattedContent = weeklyReportAiService.formatWeeklyReport( + originalText, templateKey, weekStartDate, weekEndDate); + + log.info("【周报AI格式化】格式化成功, 生成内容长度: {}", formattedContent.length()); + + JSONObject result = new JSONObject(); + result.set("formattedContent", formattedContent); + result.set("length", formattedContent.length()); + + return R.ok(result); + } catch (Exception e) { + log.error("【周报AI格式化】格式化失败", e); + return R.error("AI格式化失败: " + e.getMessage()); + } + } + + @Operation(summary = "基于日报自动生成周报") + @PostMapping("/generateFromDaily") + public R generateFromDaily(@RequestBody JSONObject data) { + log.info("【周报自动生成】开始基于日报生成周报, 请求数据: {}", data); + + try { + Long userId = data.getLong("userId"); + String templateKey = data.getStr("templateKey", "weekly_report_format"); + String weekStartDate = data.getStr("weekStartDate"); + String weekEndDate = data.getStr("weekEndDate"); + + if (userId == null) { + return R.error("userId不能为空"); + } + if (weekStartDate == null || weekEndDate == null) { + return R.error("周范围不能为空"); + } + + LocalDate start = LocalDate.parse(weekStartDate); + LocalDate end = LocalDate.parse(weekEndDate); + + // 仅聚合本周已提交的日报 + List dailyList = dailyReportService.getUserReports(userId, start, end, 1); + + if (dailyList == null || dailyList.isEmpty()) { + log.info("【周报自动生成】本周无已提交日报, userId={}, {}-{}", userId, start, end); + return R.error("本周没有已提交的日报"); + } + + // 按日期升序,拼接内容(优先用户最终编辑,其次AI格式化,最后原文) + dailyList.sort(Comparator.comparing(DailyReportEntity::getReportDate)); + + StringBuilder source = new StringBuilder(); + for (DailyReportEntity r : dailyList) { + String content = r.getUserEditedContent(); + if (content == null || content.isEmpty()) { + content = r.getAiFormattedContent(); + } + if (content == null || content.isEmpty()) { + content = r.getOriginalText(); + } + if (content != null && !content.isEmpty()) { + source.append("# ") + .append(r.getReportDate()) + .append("\n") + .append(content) + .append("\n\n"); + } + } + + if (source.length() == 0) { + return R.error("本周日报内容为空,无法生成周报"); + } + + String formatted = weeklyReportAiService.formatWeeklyReport( + source.toString(), templateKey, start.toString(), end.toString() + ); + + JSONObject result = new JSONObject(); + result.set("formattedContent", formatted); + result.set("length", formatted != null ? formatted.length() : 0); + result.set("sourceCount", dailyList.size()); + result.set("sourceContent", source.toString()); + + log.info("【周报自动生成】生成成功, 聚合日报条数: {}, 生成内容长度: {}", + dailyList.size(), formatted != null ? formatted.length() : 0); + + return R.ok(result); + } catch (Exception e) { + log.error("【周报自动生成】失败", e); + return R.error("周报自动生成失败: " + e.getMessage()); + } + } + + @Operation(summary = "获取本周工作建议") + @GetMapping("/advice") + public R getWeeklyAdvice( + @RequestParam Long userId, + @RequestParam String weekStart + ) { + try { + LocalDate start = LocalDate.parse(weekStart); + LocalDate end = start.plusDays(6); + + // 优先基于当周“已提交周报”的内容 + QueryWrapper wq = QueryWrapper.create() + .eq(WeeklyReportEntity::getUserId, userId) + .eq(WeeklyReportEntity::getWeekStartDate, start) + .eq(WeeklyReportEntity::getWeekEndDate, end) + .eq(WeeklyReportEntity::getStatus, 1); + WeeklyReportEntity weekly = weeklyReportService.getOne(wq); + + String contentForAdvice = null; + if (weekly != null) { + String c = weekly.getUserEditedContent(); + if (c == null || c.isBlank()) c = weekly.getAiFormattedContent(); + if (c == null || c.isBlank()) c = weekly.getOriginalText(); + if (c != null && !c.isBlank()) { + contentForAdvice = new StringBuilder() + .append("# 周报\n") + .append(c) + .toString(); + } + } + + // 若无周报内容,则回退到当周“已提交日报”聚合 + if (contentForAdvice == null) { + List list = dailyReportService.getUserReports(userId, start, end, 1); + StringBuilder sb = new StringBuilder(); + for (DailyReportEntity r : list) { + String c = r.getUserEditedContent(); + if (c == null || c.isBlank()) c = r.getAiFormattedContent(); + if (c == null || c.isBlank()) c = r.getOriginalText(); + if (c != null && !c.isBlank()) { + sb.append("- ").append(r.getReportDate()).append(": ") + .append(c).append("\n"); + } + } + contentForAdvice = sb.toString(); + } + + String advice = dailyReportAiService.generateAdvice("user_advice_weekly", contentForAdvice); + + JSONObject result = new JSONObject(); + result.set("userId", userId); + result.set("weekStart", start.toString()); + result.set("weekEnd", end.toString()); + result.set("adviceText", advice); + return R.ok(result); + } catch (Exception e) { + log.error("【用户端周报】获取本周建议失败", e); + return R.error("获取本周建议失败: " + e.getMessage()); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/entity/WeeklyReportEntity.java b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/entity/WeeklyReportEntity.java new file mode 100644 index 0000000..a4fac29 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/entity/WeeklyReportEntity.java @@ -0,0 +1,46 @@ +package com.cool.modules.weeklyreport.entity; + +import com.cool.core.base.BaseEntity; +import com.cool.core.annotation.ColumnDefine; +import com.mybatisflex.annotation.Table; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 周报实体类 + */ +@Getter +@Setter +@Table(value = "weekly_report", comment = "周报表") +public class WeeklyReportEntity extends BaseEntity { + + @ColumnDefine(comment = "用户ID", notNull = true) + private Long userId; + + @ColumnDefine(comment = "周开始日期(周一)", notNull = true) + private LocalDate weekStartDate; + + @ColumnDefine(comment = "周结束日期(周日)", notNull = true) + private LocalDate weekEndDate; + + @ColumnDefine(comment = "原始输入内容", type = "TEXT") + private String originalText; + + @ColumnDefine(comment = "AI格式化内容", type = "TEXT") + private String aiFormattedContent; + + @ColumnDefine(comment = "用户最终编辑内容", type = "TEXT") + private String userEditedContent; + + @ColumnDefine(comment = "状态 0-草稿 1-已提交", defaultValue = "0") + private Integer status; + + @ColumnDefine(comment = "输入方式 0-文字 1-语音", defaultValue = "0") + private Integer inputType; + + @ColumnDefine(comment = "提交时间") + private LocalDateTime submitTime; +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/mapper/WeeklyReportMapper.java b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/mapper/WeeklyReportMapper.java new file mode 100644 index 0000000..a016f02 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/mapper/WeeklyReportMapper.java @@ -0,0 +1,10 @@ +package com.cool.modules.weeklyreport.mapper; + +import com.cool.modules.weeklyreport.entity.WeeklyReportEntity; +import com.mybatisflex.core.BaseMapper; + +/** + * 周报Mapper + */ +public interface WeeklyReportMapper extends BaseMapper { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/WeeklyReportAiService.java b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/WeeklyReportAiService.java new file mode 100644 index 0000000..c0c1db3 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/WeeklyReportAiService.java @@ -0,0 +1,18 @@ +package com.cool.modules.weeklyreport.service; + +/** + * 周报AI格式化Service接口 + */ +public interface WeeklyReportAiService { + + /** + * 格式化周报内容 + * + * @param originalText 原始输入文本 + * @param templateKey 模板标识(默认: weekly_report_format) + * @param weekStartDate 周开始日期 + * @param weekEndDate 周结束日期 + * @return 格式化后的周报内容 + */ + String formatWeeklyReport(String originalText, String templateKey, String weekStartDate, String weekEndDate); +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/WeeklyReportService.java b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/WeeklyReportService.java new file mode 100644 index 0000000..3f1d689 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/WeeklyReportService.java @@ -0,0 +1,10 @@ +package com.cool.modules.weeklyreport.service; + +import com.cool.core.base.BaseService; +import com.cool.modules.weeklyreport.entity.WeeklyReportEntity; + +/** + * 周报Service接口 + */ +public interface WeeklyReportService extends BaseService { +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/impl/WeeklyReportAiServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/impl/WeeklyReportAiServiceImpl.java new file mode 100644 index 0000000..6aa5649 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/impl/WeeklyReportAiServiceImpl.java @@ -0,0 +1,193 @@ +package com.cool.modules.weeklyreport.service.impl; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.modules.aiconfig.entity.AiApiConfigEntity; +import com.cool.modules.aiconfig.entity.AiModelEntity; +import com.cool.modules.aiconfig.entity.AiTemplateEntity; +import com.cool.modules.aiconfig.service.AiApiConfigService; +import com.cool.modules.aiconfig.service.AiModelService; +import com.cool.modules.aiconfig.service.AiTemplateService; +import com.cool.modules.weeklyreport.service.WeeklyReportAiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 周报AI格式化Service实现 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeeklyReportAiServiceImpl implements WeeklyReportAiService { + + private final AiModelService aiModelService; + private final AiApiConfigService aiApiConfigService; + private final AiTemplateService aiTemplateService; + + @Override + public String formatWeeklyReport(String originalText, String templateKey, String weekStartDate, String weekEndDate) { + log.info("【周报AI格式化】开始格式化周报, templateKey={}, originalText长度={}, weekStartDate={}, weekEndDate={}", + templateKey, originalText != null ? originalText.length() : 0, weekStartDate, weekEndDate); + + try { + // 1. 获取AI模板 + AiTemplateEntity template = aiTemplateService.getByTemplateKey(templateKey); + if (template == null) { + log.error("【周报AI格式化】模板未配置, templateKey={}", templateKey); + throw new RuntimeException("周报模板未配置,请联系管理员"); + } + + log.info("【周报AI格式化】获取模板成功, templateName={}, modelId={}", + template.getTemplateName(), template.getModelId()); + + // 2. 获取AI模型配置 + AiModelEntity model = aiModelService.getById(template.getModelId()); + if (model == null) { + log.error("【周报AI格式化】AI模型未配置, modelId={}", template.getModelId()); + throw new RuntimeException("AI模型未配置"); + } + + log.info("【周报AI格式化】获取AI模型成功, modelName={}, modelKey={}", + model.getModelName(), model.getModelKey()); + + // 3. 构建提示词(替换模板中的占位符) + String prompt = buildPrompt(template.getPromptContent(), originalText, weekStartDate, weekEndDate); + log.info("【周报AI格式化】构建提示词成功, prompt长度={}", prompt.length()); + + // 4. 调用AI模型生成内容 + String formattedContent = callAiModel(model, prompt); + log.info("【周报AI格式化】AI生成成功, 生成内容长度={}", formattedContent.length()); + + return formattedContent; + } catch (Exception e) { + log.error("【周报AI格式化】格式化失败", e); + throw new RuntimeException("AI格式化失败: " + e.getMessage(), e); + } + } + + /** + * 构建提示词,替换模板中的占位符 + */ + private String buildPrompt(String templateContent, String originalText, String weekStartDate, String weekEndDate) { + log.info("【周报AI格式化】开始构建提示词"); + + String prompt = templateContent + // 支持多种内容占位符格式 + .replace("{original_text}", originalText) + .replace("{{original_text}}", originalText) + .replace("{content}", originalText) + .replace("{{content}}", originalText); + + // 替换周开始日期 + if (weekStartDate != null && !weekStartDate.isEmpty()) { + prompt = prompt + .replace("{week_start_date}", weekStartDate) + .replace("{{week_start_date}}", weekStartDate) + .replace("{start_date}", weekStartDate) + .replace("{{start_date}}", weekStartDate); + } + + // 替换周结束日期 + if (weekEndDate != null && !weekEndDate.isEmpty()) { + prompt = prompt + .replace("{week_end_date}", weekEndDate) + .replace("{{week_end_date}}", weekEndDate) + .replace("{end_date}", weekEndDate) + .replace("{{end_date}}", weekEndDate); + } + + // 替换周范围(格式:2023-11-06 至 2023-11-12) + if (weekStartDate != null && weekEndDate != null) { + String weekRange = weekStartDate + " 至 " + weekEndDate; + prompt = prompt + .replace("{week_range}", weekRange) + .replace("{{week_range}}", weekRange); + } + + log.info("【周报AI格式化】提示词构建完成"); + return prompt; + } + + /** + * 调用AI模型生成内容 + */ + private String callAiModel(AiModelEntity model, String prompt) { + log.info("【周报AI格式化】开始调用AI模型, modelKey={}", model.getModelKey()); + + try { + // 获取API配置 + AiApiConfigEntity apiConfig = aiApiConfigService.getByModelId(model.getId()); + if (apiConfig == null) { + log.error("【周报AI格式化】API配置不存在, modelId={}", model.getId()); + throw new RuntimeException("AI模型API配置不存在"); + } + + log.info("【周报AI格式化】获取API配置成功, apiUrl={}", apiConfig.getApiUrl()); + + // 构建请求体(通义千问格式) + JSONObject requestBody = new JSONObject(); + requestBody.set("model", model.getModelKey()); + + JSONObject input = new JSONObject(); + JSONArray messages = new JSONArray(); + JSONObject message = new JSONObject(); + message.set("role", "user"); + message.set("content", prompt); + messages.add(message); + input.set("messages", messages); + requestBody.set("input", input); + + // 添加参数 + JSONObject parameters = new JSONObject(); + parameters.set("max_tokens", 2000); + requestBody.set("parameters", parameters); + + log.info("【周报AI格式化】发送AI请求, requestBody={}", requestBody.toString()); + + // 发送HTTP请求 + HttpRequest request = HttpRequest.post(apiConfig.getApiUrl()) + .header("Authorization", "Bearer " + apiConfig.getApiKey()) + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .timeout(120000); + + HttpResponse response = request.execute(); + String responseBody = response.body(); + + log.info("【周报AI格式化】收到AI响应, status={}, responseBody={}", + response.getStatus(), responseBody); + + if (!response.isOk()) { + log.error("【周报AI格式化】AI请求失败, status={}, body={}", + response.getStatus(), responseBody); + throw new RuntimeException("AI模型调用失败: HTTP " + response.getStatus()); + } + + // 解析响应 + JSONObject result = JSONUtil.parseObj(responseBody); + + // 尝试从 output.text 获取内容 + String content = result.getByPath("output.text", String.class); + + // 如果没有,尝试从 output.choices[0].message.content 获取 + if (content == null || content.isEmpty()) { + content = result.getByPath("output.choices[0].message.content", String.class); + } + + if (content == null || content.isEmpty()) { + log.error("【周报AI格式化】AI响应中未找到生成内容, response={}", responseBody); + throw new RuntimeException("AI模型返回内容为空"); + } + + log.info("【周报AI格式化】AI调用成功, 生成内容长度={}", content.length()); + return content; + } catch (Exception e) { + log.error("【周报AI格式化】AI模型调用失败", e); + throw new RuntimeException("AI模型调用失败: " + e.getMessage(), e); + } + } +} diff --git a/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/impl/WeeklyReportServiceImpl.java b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/impl/WeeklyReportServiceImpl.java new file mode 100644 index 0000000..0f42891 --- /dev/null +++ b/cool-admin-java/src/main/java/com/cool/modules/weeklyreport/service/impl/WeeklyReportServiceImpl.java @@ -0,0 +1,20 @@ +package com.cool.modules.weeklyreport.service.impl; + +import com.cool.core.base.BaseServiceImpl; +import com.cool.modules.weeklyreport.entity.WeeklyReportEntity; +import com.cool.modules.weeklyreport.mapper.WeeklyReportMapper; +import com.cool.modules.weeklyreport.service.WeeklyReportService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 周报Service实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeeklyReportServiceImpl + extends BaseServiceImpl + implements WeeklyReportService { +} diff --git a/cool-admin-java/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java b/cool-admin-java/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java new file mode 100644 index 0000000..2d01e23 --- /dev/null +++ b/cool-admin-java/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +import java.util.Map; +import java.util.Properties; + +import javax.sql.DataSource; + +import com.cool.core.util.DatabaseDialectUtils; +import org.quartz.Calendar; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.Trigger; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.OnDatabaseInitializationCondition; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.scheduling.quartz.SpringBeanJobFactory; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Quartz Scheduler. + * 覆盖原始的QuartzAutoConfiguration,兼容postgres数据库 + * properties.getProperties().put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"); + * @author Vedran Pavic + * @author Stephane Nicoll + * @since 2.0.0 + */ +@AutoConfiguration(after = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, + LiquibaseAutoConfiguration.class, FlywayAutoConfiguration.class }) +@ConditionalOnClass({ Scheduler.class, SchedulerFactoryBean.class, PlatformTransactionManager.class }) +@EnableConfigurationProperties(QuartzProperties.class) +public class QuartzAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public SchedulerFactoryBean quartzScheduler(QuartzProperties properties, + ObjectProvider customizers, ObjectProvider jobDetails, + Map calendars, ObjectProvider triggers, ApplicationContext applicationContext) { + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); + SpringBeanJobFactory jobFactory = new SpringBeanJobFactory(); + jobFactory.setApplicationContext(applicationContext); + schedulerFactoryBean.setJobFactory(jobFactory); + if (properties.getSchedulerName() != null) { + schedulerFactoryBean.setSchedulerName(properties.getSchedulerName()); + } + schedulerFactoryBean.setAutoStartup(properties.isAutoStartup()); + schedulerFactoryBean.setStartupDelay((int) properties.getStartupDelay().getSeconds()); + schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(properties.isWaitForJobsToCompleteOnShutdown()); + schedulerFactoryBean.setOverwriteExistingJobs(properties.isOverwriteExistingJobs()); + if (!properties.getProperties().isEmpty()) { + schedulerFactoryBean.setQuartzProperties(asProperties(properties.getProperties())); + } + schedulerFactoryBean.setJobDetails(jobDetails.orderedStream().toArray(JobDetail[]::new)); + schedulerFactoryBean.setCalendars(calendars); + schedulerFactoryBean.setTriggers(triggers.orderedStream().toArray(Trigger[]::new)); + customizers.orderedStream().forEach((customizer) -> customizer.customize(schedulerFactoryBean)); + return schedulerFactoryBean; + } + + private Properties asProperties(Map source) { + Properties properties = new Properties(); + properties.putAll(source); + return properties; + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnSingleCandidate(DataSource.class) + @ConditionalOnProperty(prefix = "spring.quartz", name = "job-store-type", havingValue = "jdbc") + @Import(DatabaseInitializationDependencyConfigurer.class) + protected static class JdbcStoreTypeConfiguration { + + @Bean + @Order(0) + public SchedulerFactoryBeanCustomizer dataSourceCustomizer(QuartzProperties properties, DataSource dataSource, + @QuartzDataSource ObjectProvider quartzDataSource, + ObjectProvider transactionManager, + @QuartzTransactionManager ObjectProvider quartzTransactionManager) { + return (schedulerFactoryBean) -> { + DataSource dataSourceToUse = getDataSource(dataSource, quartzDataSource); + schedulerFactoryBean.setDataSource(dataSourceToUse); + PlatformTransactionManager txManager = getTransactionManager(transactionManager, + quartzTransactionManager); + if (txManager != null) { + schedulerFactoryBean.setTransactionManager(txManager); + } + }; + } + + private DataSource getDataSource(DataSource dataSource, ObjectProvider quartzDataSource) { + DataSource dataSourceIfAvailable = quartzDataSource.getIfAvailable(); + return (dataSourceIfAvailable != null) ? dataSourceIfAvailable : dataSource; + } + + private PlatformTransactionManager getTransactionManager( + ObjectProvider transactionManager, + ObjectProvider quartzTransactionManager) { + PlatformTransactionManager transactionManagerIfAvailable = quartzTransactionManager.getIfAvailable(); + return (transactionManagerIfAvailable != null) ? transactionManagerIfAvailable + : transactionManager.getIfUnique(); + } + + @Bean + @ConditionalOnMissingBean(QuartzDataSourceScriptDatabaseInitializer.class) + @Conditional(OnQuartzDatasourceInitializationCondition.class) + public QuartzDataSourceScriptDatabaseInitializer quartzDataSourceScriptDatabaseInitializer( + DataSource dataSource, @QuartzDataSource ObjectProvider quartzDataSource, + QuartzProperties properties) { + DataSource dataSourceToUse = getDataSource(dataSource, quartzDataSource); + if (DatabaseDialectUtils.isPostgresql(dataSource)) { + properties.getProperties().put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"); + } + return new QuartzDataSourceScriptDatabaseInitializer(dataSourceToUse, properties); + } + + static class OnQuartzDatasourceInitializationCondition extends OnDatabaseInitializationCondition { + + OnQuartzDatasourceInitializationCondition() { + super("Quartz", "spring.quartz.jdbc.initialize-schema"); + } + + } + + } + +} diff --git a/cool-admin-java/src/main/resources/application-local.yml b/cool-admin-java/src/main/resources/application-local.yml new file mode 100644 index 0000000..41e556f --- /dev/null +++ b/cool-admin-java/src/main/resources/application-local.yml @@ -0,0 +1,45 @@ +spring: + datasource: + url: jdbc:postgresql://118.190.148.29:5432/cool-jin + username: postgres + password: wheelike_2021 + driver-class-name: org.postgresql.Driver + hikari: + max-lifetime: 180000 # 30分钟,应小于数据库的超时时间 + validation-timeout: 3000 + connection-timeout: 60000 + idle-timeout: 600000 + initialization-fail-timeout: 30000 + connection-init-sql: SELECT 1 +# AutoTable配置,根据实体类自动生成表 +auto-table: + # 启用自动维护表功能 + enable: true + # 自动删除名称不匹配的索引 + autoDropIndex: true + # 建表的时候,父类的字段排序是在子类后面还是前面 + superInsertPosition: before + # 模型包路径 + model-package: com.cool.**.entity + +# Cool相关配置 +cool: + # 初始化数据 + initData: true + +# 文档 +springdoc: + api-docs: + #是否开启文档功能 本地为了配合eps功能不可关闭 + enabled: true + group-configs: + - group: app + paths-to-match: /app/** + - group: admin + paths-to-match: /admin/** + +# 设置日志级别 +logging: + level: + com.cool: debug + com.zaxxer.hikari: debug \ No newline at end of file diff --git a/cool-admin-java/src/main/resources/application-prod.yml b/cool-admin-java/src/main/resources/application-prod.yml new file mode 100644 index 0000000..7d4b34d --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/resources/application.yml b/cool-admin-java/src/main/resources/application.yml new file mode 100644 index 0000000..ea8c85c --- /dev/null +++ b/cool-admin-java/src/main/resources/application.yml @@ -0,0 +1,162 @@ +server: + port: 8001 + servlet: + context-path: / + compression: + enabled: true + mime-types: application/json,application/xml,text/html,text/plain + +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: 5 + threadPriority: 9 + threadsInheritContextClassLoaderOfInitializingThread: true + +# 忽略url +ignored: + # 忽略后台鉴权url + adminAuthUrls: + - / + - /upload/** + - /actuator/** + - /download/** + - /static/** + - /favicon.ico + - /v3/api-docs/** + - /swagger + - /swagger-ui/** + - /css/* + - /js/* + - /druid/** + - /admin/base/open/** + # 忽略记录请求日志url + logUrls: + - / + - /**/eps + - /app/** + - /css/* + - /js/* + - /favicon.ico +# 文档 +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.**.entity.* + global-config: + print-banner: false + + +# Cool相关配置 +cool: + # 缓存名称 + cacheName: comm + plugin: + # 插件安装位置 + path: assets/plugin + # token 相关配置 + token: + # 过期时间 单位:秒 半小时 + expire: 1800 + # 刷新token过期时间 单位:秒 7天 + refreshExpire: 604800 + # 文件上传相关 + file: + #上传模式 + mode: local + # 本地上传配置 + local: + # 文件访问地址 + base-url: http://127.0.0.1:${server.port}/upload + # 系统日志请求参数超过1024字节 就不记录,避免日志过大 + log: + # 请求参数最大字节,超过请求参数不记录 + max-byte-length: 1024 + # 核心线程数的倍数 + core-pool-size-multiplier: 2 + # 最大线程数的倍数 + max-pool-size-multiplier: 3 + # 队列容量的倍数 + queue-capacity-multiplier: 3 + multi-tenant: + # 是否开启多租户,默认关闭 + enable: false +# AutoTable配置,根据实体类自动生成表 +auto-table: + show-banner: false + +# 分布式唯一ID组件 +leaf: + segment: + # 默认禁用 + enable: false \ No newline at end of file diff --git a/cool-admin-java/src/main/resources/banner.txt b/cool-admin-java/src/main/resources/banner.txt new file mode 100644 index 0000000..547703b --- /dev/null +++ b/cool-admin-java/src/main/resources/banner.txt @@ -0,0 +1,8 @@ +🚀🚀🚀 ✨🌈✨[cool-admin-java-plus](https://gitee.com/hlc4417/cool-admin-java-plus)✨🌈✨ 🚀🚀🚀 + ______ ___ ___ _____ _ ______ ____ ____ _____ ____ _____ + .' ___ | .' `. .' `.|_ _| V8.x / \ |_ _ `.|_ \ / _||_ _||_ \|_ _| +/ .' \_|/ .-. \/ .-. \ | | ______ / _ \ | | `. \ | \/ | | | | \ | | +| | | | | || | | | | | _|______|/ ___ \ | | | | | |\ /| | | | | |\ \| | +\ `.___.'\\ `-' /\ `-' /_| |__/ | _/ / \ \_ _| |_.' /_| |_\/_| |_ _| |_ _| |_\ |_ + `.____ .' `.___.' `.___.'|________| |____| |____||______.'|_____||_____||_____||_____|\____| +:: https://java.cool-admin.com :: \ No newline at end of file diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/48.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/48.jpg new file mode 100644 index 0000000..ae731e4 Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/48.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/a.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/a.jpg new file mode 100644 index 0000000..cef38ec Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/a.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/b.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/b.jpg new file mode 100644 index 0000000..72fdb56 Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/b.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/c.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/c.jpg new file mode 100644 index 0000000..7c8274f Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/c.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/d.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/d.jpg new file mode 100644 index 0000000..6057faa Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/d.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/e.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/e.jpg new file mode 100644 index 0000000..b5e22e3 Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/e.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/g.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/g.jpg new file mode 100644 index 0000000..ae707ba Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/g.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/h.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/h.jpg new file mode 100644 index 0000000..2326c88 Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/h.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/i.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/i.jpg new file mode 100644 index 0000000..f9714d9 Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/i.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/captcha/bgimages/j.jpg b/cool-admin-java/src/main/resources/cool/captcha/bgimages/j.jpg new file mode 100644 index 0000000..8f9056f Binary files /dev/null and b/cool-admin-java/src/main/resources/cool/captcha/bgimages/j.jpg differ diff --git a/cool-admin-java/src/main/resources/cool/code/controller.th b/cool-admin-java/src/main/resources/cool/code/controller.th new file mode 100644 index 0000000..27cf579 --- /dev/null +++ b/cool-admin-java/src/main/resources/cool/code/controller.th @@ -0,0 +1,22 @@ +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.core.enums.Apis; +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 = {Apis.ADD, Apis.DELETE, Apis.UPDATE, Apis.PAGE, Apis.LIST, Apis.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/cool-admin-java/src/main/resources/cool/code/mapper/interface.th b/cool-admin-java/src/main/resources/cool/code/mapper/interface.th new file mode 100644 index 0000000..112e6ab --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/resources/cool/code/service/impl.th b/cool-admin-java/src/main/resources/cool/code/service/impl.th new file mode 100644 index 0000000..1c31ea7 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/resources/cool/code/service/interface.th b/cool-admin-java/src/main/resources/cool/code/service/interface.th new file mode 100644 index 0000000..6b365b0 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/resources/cool/data/db/base.json b/cool-admin-java/src/main/resources/cool/data/db/base.json new file mode 100644 index 0000000..5698188 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/resources/cool/data/menu/menu.json b/cool-admin-java/src/main/resources/cool/data/menu/menu.json new file mode 100644 index 0000000..5da8568 --- /dev/null +++ b/cool-admin-java/src/main/resources/cool/data/menu/menu.json @@ -0,0 +1,813 @@ +[ + { + "name": "系统管理", + "router": "/sys", + "perms": null, + "type": 0, + "icon": "icon-set", + "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": [] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/cool-admin-java/src/main/resources/cool/data/menu/menu_8.0.json b/cool-admin-java/src/main/resources/cool/data/menu/menu_8.0.json new file mode 100644 index 0000000..4ad1c60 --- /dev/null +++ b/cool-admin-java/src/main/resources/cool/data/menu/menu_8.0.json @@ -0,0 +1,158 @@ +[ + { + "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:type:delete,dict:type:update,dict:type:info,dict:type:list,dict:type:page,dict:type: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": [ + { + "tenantId": null, + "name": "插件列表", + "router": "/helper/plugins", + "perms": null, + "type": 1, + "icon": "icon-list", + "orderNum": 1, + "viewPath": "modules/helper/views/plugins.vue", + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "tenantId": null, + "name": "删除", + "router": null, + "perms": "plugin:info:delete", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "tenantId": null, + "name": "分页查询", + "router": null, + "perms": "plugin:info:page", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "tenantId": null, + "name": "单个信息", + "router": null, + "perms": "plugin:info:info", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "tenantId": null, + "name": "安装插件", + "router": null, + "perms": "plugin:info:install", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "tenantId": null, + "name": "修改", + "router": null, + "perms": "plugin:info:update", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "tenantId": null, + "name": "列表查询", + "router": null, + "perms": "plugin:info:list", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + }, + { + "tenantId": null, + "name": "新增", + "router": null, + "perms": "plugin:info:add", + "type": 2, + "icon": null, + "orderNum": 0, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/cool-admin-java/src/main/resources/static/css/welcome.css b/cool-admin-java/src/main/resources/static/css/welcome.css new file mode 100644 index 0000000..c838986 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/resources/static/favicon.ico b/cool-admin-java/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..c4d673d Binary files /dev/null and b/cool-admin-java/src/main/resources/static/favicon.ico differ diff --git a/cool-admin-java/src/main/resources/static/js/welcome.js b/cool-admin-java/src/main/resources/static/js/welcome.js new file mode 100644 index 0000000..eb9b697 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/main/resources/templates/welcome.html b/cool-admin-java/src/main/resources/templates/welcome.html new file mode 100644 index 0000000..2bd2140 --- /dev/null +++ b/cool-admin-java/src/main/resources/templates/welcome.html @@ -0,0 +1,25 @@ + + + + + + + + COOL-ADMIN 一个很酷的后台管理系统开发框架 + + + + + +
HELLO COOL-ADMIN V8.x
+ + + + + + diff --git a/cool-admin-java/src/test/java/com/cool/CoolAdminJavaApplicationTests.java b/cool-admin-java/src/test/java/com/cool/CoolAdminJavaApplicationTests.java new file mode 100644 index 0000000..1d280e5 --- /dev/null +++ b/cool-admin-java/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/cool-admin-java/src/test/java/com/cool/CoolCodeGeneratorTest.java b/cool-admin-java/src/test/java/com/cool/CoolCodeGeneratorTest.java new file mode 100644 index 0000000..acf8caf --- /dev/null +++ b/cool-admin-java/src/test/java/com/cool/CoolCodeGeneratorTest.java @@ -0,0 +1,56 @@ +package com.cool; + +import com.cool.core.code.CodeGenerator; +import com.cool.core.code.CodeModel; +import com.cool.core.code.CodeTypeEnum; +import com.cool.modules.user.entity.*; +import com.mybatisflex.annotation.Table; +import java.util.List; + +public class CoolCodeGeneratorTest { + public static void main(String[] args) { + CodeGenerator codeGenerator = new CodeGenerator(); + codeGenerator.init(); + List list = List.of(UserWxEntity.class); + + list.forEach(o -> { + Table annotation = (Table) o.getAnnotation(Table.class); + CodeModel codeModel = new CodeModel(); + codeModel.setType(CodeTypeEnum.APP); + codeModel.setName(annotation.comment()); + codeModel.setModule(getFirstWord(o.getSimpleName())); + codeModel.setEntity(o); + // 生成 controller +// codeGenerator.controller(codeModel); + // 生成 mapper + codeGenerator.mapper(codeModel); + // 生成 service + codeGenerator.service(codeModel); + }); + } + + public static String getFirstWord(String className) { + if (className == null || className.isEmpty()) { + return ""; + } + + StringBuilder firstWord = new StringBuilder(); + boolean foundFirstWord = false; + + for (char c : className.toCharArray()) { + if (Character.isUpperCase(c)) { + if (foundFirstWord) { + break; + } + firstWord.append(c); + foundFirstWord = true; + } else { + if (foundFirstWord) { + firstWord.append(c); + } + } + } + + return firstWord.toString().toLowerCase(); + } +}