wip:临时提交
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,4 +15,5 @@ logs
|
|||||||
|
|
||||||
docker/*/data/
|
docker/*/data/
|
||||||
docker/minio/config
|
docker/minio/config
|
||||||
docker/xxljob/logs
|
docker/xxljob/logs
|
||||||
|
application-youlai.yml
|
||||||
16
Dockerfile
16
Dockerfile
@@ -1,25 +1,23 @@
|
|||||||
# 基础镜像
|
# 基础镜像
|
||||||
FROM openjdk:17-jdk-alpine
|
FROM openjdk:17
|
||||||
|
|
||||||
# 维护者信息
|
# 维护者信息
|
||||||
MAINTAINER youlai <youlaitech@163.com>
|
MAINTAINER youlai <youlaitech@163.com>
|
||||||
|
|
||||||
# 设置国内镜像源(中国科技大学镜像源),修改容器时区(alpine镜像需安装tzdata来设置时区),安装字体库(验证码)
|
# 设置时区(Debian直接使用环境变量)
|
||||||
RUN echo -e https://mirrors.ustc.edu.cn/alpine/v3.7/main/ > /etc/apk/repositories \
|
ENV TZ=Asia/Shanghai
|
||||||
&& apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone \
|
|
||||||
&& apk --no-cache add ttf-dejavu fontconfig
|
|
||||||
|
|
||||||
# 在运行时自动挂载 /tmp 目录为匿名卷,提高可移植性。如果 /tmp 目录没有挂载为卷,这些文件会写入容器的可写层,可能导致容器镜像膨胀。
|
# 在运行时自动挂载 /tmp 目录为匿名卷
|
||||||
VOLUME /tmp
|
VOLUME /tmp
|
||||||
|
|
||||||
# 将构建的 Spring Boot 可执行 JAR 复制到容器中,重命名为 app.jar
|
# 添加应用
|
||||||
ADD target/youlai-boot.jar app.jar
|
ADD target/youlai-boot.jar app.jar
|
||||||
|
|
||||||
# 指定容器启动时执行的命令
|
# 启动命令
|
||||||
CMD java \
|
CMD java \
|
||||||
-Xms512m -Xmx512m \
|
-Xms512m -Xmx512m \
|
||||||
-Djava.security.egd=file:/dev/./urandom \
|
-Djava.security.egd=file:/dev/./urandom \
|
||||||
-jar /app.jar
|
-jar /app.jar
|
||||||
|
|
||||||
# 暴露容器的端口
|
# 暴露端口
|
||||||
EXPOSE 8989
|
EXPOSE 8989
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -1,9 +1,34 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
## 🎉 正在参加 Gitee 2025 最受欢迎开源软件评选
|
||||||
|
|
||||||
|
<a href="https://gitee.com/activity/2025opensource?ident=I6VXEH" target="_blank">
|
||||||
|
<img src="https://img.shields.io/badge/🗳️_立即投票-支持本项目-ff6b35?style=for-the-badge&logo=gitee" alt="投票" height="50"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>一票就够,不用每天投 🙏 您的支持是我们持续更新的最大动力!</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://gitee.com/activity/2025opensource?ident=I6VXEH" target="_blank">
|
||||||
|
<strong>👉 点击徽章或这里投票 👈</strong>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="logo" width="100" height="100" src="https://foruda.gitee.com/images/1733417239320800627/3c5290fe_716974.png">
|
<img alt="logo" width="100" height="100" src="https://foruda.gitee.com/images/1733417239320800627/3c5290fe_716974.png">
|
||||||
<h2>youlai-boot</h2>
|
<h2>youlai-boot</h2>
|
||||||
<img alt="有来技术" src="https://img.shields.io/badge/Java -17-brightgreen.svg"/>
|
<img alt="有来技术" src="https://img.shields.io/badge/Java -17-brightgreen.svg"/>
|
||||||
<img alt="有来技术" src="https://img.shields.io/badge/SpringBoot-3.3.6-green.svg"/>
|
<img alt="有来技术" src="https://img.shields.io/badge/SpringBoot-3.5.6-green.svg"/>
|
||||||
|
<a href="https://gitcode.com/youlai/youlai-boot" target="_blank">
|
||||||
|
<img alt="有来技术" src="https://gitcode.com/youlai/youlai-boot/star/badge.svg"/>
|
||||||
|
</a>
|
||||||
<a href="https://gitee.com/youlaiorg/youlai-boot" target="_blank">
|
<a href="https://gitee.com/youlaiorg/youlai-boot" target="_blank">
|
||||||
<img alt="有来技术" src="https://gitee.com/youlaiorg/youlai-boot/badge/star.svg"/>
|
<img alt="有来技术" src="https://gitee.com/youlaiorg/youlai-boot/badge/star.svg"/>
|
||||||
</a>
|
</a>
|
||||||
@@ -37,8 +62,8 @@
|
|||||||
|
|
||||||
## 🌈 项目源码
|
## 🌈 项目源码
|
||||||
|
|
||||||
| 项目类型 | Gitee | Github | GitCode |
|
| 项目类型 | Gitee | Github | GitCode |
|
||||||
| --------------| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
| --------------| ------------------------- | ------------------------------- | ------------------------------------- |
|
||||||
| ✅ Java 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot) | [youlai-boot](https://gitcode.com/youlai/youlai-boot) |
|
| ✅ Java 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot) | [youlai-boot](https://gitcode.com/youlai/youlai-boot) |
|
||||||
| vue3 前端 | [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) |
|
| vue3 前端 | [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) |
|
||||||
| uni-app 移动端 | [vue-uniapp-template](https://gitee.com/youlaiorg/vue-uniapp-template) | [vue-uniapp-template](https://github.com/youlaitech/vue-uniapp-template) | [vue-uniapp-template](https://gitcode.com/youlai/vue-uniapp-template) |
|
| uni-app 移动端 | [vue-uniapp-template](https://gitee.com/youlaiorg/vue-uniapp-template) | [vue-uniapp-template](https://github.com/youlaitech/vue-uniapp-template) | [vue-uniapp-template](https://gitcode.com/youlai/vue-uniapp-template) |
|
||||||
@@ -70,47 +95,55 @@ youlai-boot
|
|||||||
├── sql # SQL脚本
|
├── sql # SQL脚本
|
||||||
│ ├── mysql # MySQL 脚本
|
│ ├── mysql # MySQL 脚本
|
||||||
├── src # 源码目录
|
├── src # 源码目录
|
||||||
|
│ ├── auth # 认证模块(登录入口)
|
||||||
│ ├── common # 公共模块
|
│ ├── common # 公共模块
|
||||||
│ │ ├── annotation # 注解定义
|
│ │ ├── annotation # 注解定义
|
||||||
│ │ ├── base # 基础类
|
│ │ ├── base # 基础类
|
||||||
│ │ ├── constant # 常量
|
│ │ ├── constant # 常量
|
||||||
│ │ ├── enums # 枚举类型
|
│ │ ├── enums # 枚举类型
|
||||||
│ │ ├── exception # 异常处理
|
|
||||||
│ │ ├── model # 数据模型
|
│ │ ├── model # 数据模型
|
||||||
│ │ ├── result # 结果封装
|
|
||||||
│ │ └── util # 工具类
|
│ │ └── util # 工具类
|
||||||
│ ├── config # 自动装配配置
|
│ ├── config # 自动装配配置
|
||||||
│ │ └── property # 配置属性目录
|
│ │ └── property # 配置属性目录
|
||||||
│ ├── core # 核心功能
|
│ ├── core # 核心框架
|
||||||
│ │ ├── aspect # 切面(日志、防重提交)
|
│ │ ├── aspect # 切面(日志、防重提交)
|
||||||
|
│ │ ├── exception # 异常处理
|
||||||
│ │ ├── filter # 过滤器(请求日志、限流)
|
│ │ ├── filter # 过滤器(请求日志、限流)
|
||||||
│ │ ├── handler # 处理器(数据权限、数据填充)
|
│ │ ├── validator # 验证器
|
||||||
│ │ └── security # Spring Security 安全模块
|
│ │ └── web # Web响应封装(Result、PageResult等)
|
||||||
│ ├── modules # 业务模块
|
│ ├── platform # 平台服务(通用服务)
|
||||||
│ │ ├── member # 会员模块【业务模块演示】
|
|
||||||
│ │ ├── order # 订单模块【业务模块演示】
|
|
||||||
│ │ ├── product # 商品模块【业务模块演示】
|
|
||||||
│ ├── shared # 共享模块
|
|
||||||
│ │ ├── auth # 认证模块
|
|
||||||
│ │ ├── file # 文件模块
|
|
||||||
│ │ ├── codegen # 代码生成模块
|
│ │ ├── codegen # 代码生成模块
|
||||||
│ │ ├── mail # 邮件模块
|
│ │ ├── file # 文件服务
|
||||||
│ │ ├── sms # 短信模块
|
│ │ ├── mail # 邮件服务
|
||||||
│ │ └── websocket # WebSocket 模块
|
│ │ ├── sms # 短信服务
|
||||||
|
│ │ └── websocket # WebSocket服务
|
||||||
|
│ ├── plugin # 插件扩展
|
||||||
|
│ │ ├── knife4j # Knife4j 扩展
|
||||||
|
│ │ └── mybatis # Mybatis 扩展
|
||||||
|
│ ├── security # 安全框架(Spring Security)
|
||||||
|
│ │ ├── exception # 安全异常
|
||||||
|
│ │ ├── filter # 安全过滤器
|
||||||
|
│ │ ├── handler # 安全处理器
|
||||||
|
│ │ ├── model # 安全模型
|
||||||
|
│ │ ├── provider # 认证提供者
|
||||||
|
│ │ ├── service # 安全服务
|
||||||
|
│ │ ├── token # Token管理
|
||||||
|
│ │ └── util # 安全工具类
|
||||||
│ ├── system # 系统模块
|
│ ├── system # 系统模块
|
||||||
│ │ ├── controller # 控制层
|
│ │ ├── controller # 控制层
|
||||||
│ │ ├── converter # MapStruct 转换器
|
│ │ ├── converter # MapStruct 转换器
|
||||||
│ │ ├── event # 事件处理
|
│ │ ├── enums # 枚举
|
||||||
│ │ ├── handler # 处理器
|
│ │ ├── handler # 处理器
|
||||||
│ │ ├── listener # 监听器
|
│ │ ├── listener # 监听器
|
||||||
|
│ │ ├── mapper # 数据库访问层
|
||||||
│ │ ├── model # 模型层
|
│ │ ├── model # 模型层
|
||||||
│ │ │ ├── bo # 业务对象
|
│ │ │ ├── bo # 业务对象
|
||||||
│ │ │ ├── dto # 数据传输对象
|
│ │ │ ├── dto # 数据传输对象
|
||||||
│ │ │ ├── entity # 实体对象
|
│ │ │ ├── entity # 实体对象
|
||||||
|
│ │ │ ├── event # 事件对象
|
||||||
│ │ │ ├── form # 表单对象
|
│ │ │ ├── form # 表单对象
|
||||||
│ │ │ ├── query # 查询参数对象
|
│ │ │ ├── query # 查询参数对象
|
||||||
│ │ │ └── vo # 视图对象
|
│ │ │ └── vo # 视图对象
|
||||||
│ │ ├── mapper # 数据库访问层
|
|
||||||
│ │ └── service # 业务逻辑层
|
│ │ └── service # 业务逻辑层
|
||||||
│ └── YouLaiBootApplication # 启动类
|
│ └── YouLaiBootApplication # 启动类
|
||||||
└── end
|
└── end
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ services:
|
|||||||
- youlai-boot
|
- youlai-boot
|
||||||
|
|
||||||
minio:
|
minio:
|
||||||
image: minio/minio:latest
|
image: minio/minio:RELEASE.2024-07-16T23-46-41Z
|
||||||
container_name: minio
|
container_name: minio
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server /data --console-address ":9001"
|
command: server /data --console-address ":9001"
|
||||||
@@ -66,4 +66,4 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
networks:
|
networks:
|
||||||
- youlai-boot
|
- youlai-boot
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ protected-mode no
|
|||||||
#
|
#
|
||||||
# enable-protected-configs no
|
# enable-protected-configs no
|
||||||
# enable-debug-command no
|
# enable-debug-command no
|
||||||
# enable-module-command no
|
# enable-business-command no
|
||||||
|
|
||||||
# Accept connections on the specified port, default is 6379 (IANA #815344).
|
# Accept connections on the specified port, default is 6379 (IANA #815344).
|
||||||
# If port 0 is specified Redis will not listen on a TCP socket.
|
# If port 0 is specified Redis will not listen on a TCP socket.
|
||||||
@@ -867,7 +867,7 @@ replica-priority 100
|
|||||||
# Warning: since Redis is pretty fast, an outside user can try up to
|
# Warning: since Redis is pretty fast, an outside user can try up to
|
||||||
# 1 million passwords per second against a modern box. This means that you
|
# 1 million passwords per second against a modern box. This means that you
|
||||||
# should use very strong passwords, otherwise they will be very easy to break.
|
# should use very strong passwords, otherwise they will be very easy to break.
|
||||||
# Note that because the password is really a shared secret between the client
|
# Note that because the password is really a platform secret between the client
|
||||||
# and the server, and should not be memorized by any human, the password
|
# and the server, and should not be memorized by any human, the password
|
||||||
# can be easily a long string from /dev/urandom or whatever, so by using a
|
# can be easily a long string from /dev/urandom or whatever, so by using a
|
||||||
# long and unguessable password no brute force attack will be possible.
|
# long and unguessable password no brute force attack will be possible.
|
||||||
@@ -964,7 +964,7 @@ replica-priority 100
|
|||||||
#
|
#
|
||||||
# user alice on +@all -DEBUG ~* >somepassword
|
# user alice on +@all -DEBUG ~* >somepassword
|
||||||
#
|
#
|
||||||
# This will allow "alice" to use all the commands with the exception of the
|
# This will allow "alice" to use all the commands with the handler of the
|
||||||
# DEBUG command, since +@all added all the commands to the set of the commands
|
# DEBUG command, since +@all added all the commands to the set of the commands
|
||||||
# alice can use, and later DEBUG was removed. However if we invert the order
|
# alice can use, and later DEBUG was removed. However if we invert the order
|
||||||
# of two ACL rules the result will be different:
|
# of two ACL rules the result will be different:
|
||||||
@@ -1066,7 +1066,7 @@ acllog-max-len 128
|
|||||||
# create for administrative purposes.
|
# create for administrative purposes.
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# It is possible to change the name of dangerous commands in a shared
|
# It is possible to change the name of dangerous commands in a platform
|
||||||
# environment. For instance the CONFIG command may be renamed into something
|
# environment. For instance the CONFIG command may be renamed into something
|
||||||
# hard to guess so that it will still be available for internal-use tools
|
# hard to guess so that it will still be available for internal-use tools
|
||||||
# but not available for general clients.
|
# but not available for general clients.
|
||||||
@@ -1095,7 +1095,7 @@ acllog-max-len 128
|
|||||||
# an error 'max number of clients reached'.
|
# an error 'max number of clients reached'.
|
||||||
#
|
#
|
||||||
# IMPORTANT: When Redis Cluster is used, the max number of connections is also
|
# IMPORTANT: When Redis Cluster is used, the max number of connections is also
|
||||||
# shared with the cluster bus: every node in the cluster will use two
|
# platform with the cluster bus: every node in the cluster will use two
|
||||||
# connections, one incoming and another outgoing. It is important to size the
|
# connections, one incoming and another outgoing. It is important to size the
|
||||||
# limit accordingly in case of very large clusters.
|
# limit accordingly in case of very large clusters.
|
||||||
#
|
#
|
||||||
@@ -1563,7 +1563,7 @@ aof-timestamp-enabled no
|
|||||||
#
|
#
|
||||||
# In this state Redis will only allow a handful of commands to be executed.
|
# In this state Redis will only allow a handful of commands to be executed.
|
||||||
# For instance, SCRIPT KILL, FUNCTION KILL, SHUTDOWN NOSAVE and possibly some
|
# For instance, SCRIPT KILL, FUNCTION KILL, SHUTDOWN NOSAVE and possibly some
|
||||||
# module specific 'allow-busy' commands.
|
# business specific 'allow-busy' commands.
|
||||||
#
|
#
|
||||||
# SCRIPT KILL and FUNCTION KILL will only be able to stop a script that did not
|
# SCRIPT KILL and FUNCTION KILL will only be able to stop a script that did not
|
||||||
# yet call any write commands, so SHUTDOWN NOSAVE may be the only way to stop
|
# yet call any write commands, so SHUTDOWN NOSAVE may be the only way to stop
|
||||||
|
|||||||
35
pom.xml
35
pom.xml
@@ -6,13 +6,13 @@
|
|||||||
|
|
||||||
<groupId>com.youlai</groupId>
|
<groupId>com.youlai</groupId>
|
||||||
<artifactId>youlai-boot</artifactId>
|
<artifactId>youlai-boot</artifactId>
|
||||||
<version>2.22.0</version>
|
<version>3.3.0</version>
|
||||||
<description>基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。</description>
|
<description>基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。</description>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.3.6</version> <!-- lookup parent from repository -->
|
<version>3.5.6</version> <!-- lookup parent from repository -->
|
||||||
<relativePath/>
|
<relativePath/>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -25,15 +25,16 @@
|
|||||||
<mysql-connector-j.version>9.1.0</mysql-connector-j.version>
|
<mysql-connector-j.version>9.1.0</mysql-connector-j.version>
|
||||||
<druid.version>1.2.24</druid.version>
|
<druid.version>1.2.24</druid.version>
|
||||||
<mybatis-plus.version>3.5.5</mybatis-plus.version>
|
<mybatis-plus.version>3.5.5</mybatis-plus.version>
|
||||||
|
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
|
||||||
|
|
||||||
<knife4j.version>4.5.0</knife4j.version>
|
<knife4j.version>4.5.0</knife4j.version>
|
||||||
|
|
||||||
<mapstruct.version>1.6.3</mapstruct.version>
|
<mapstruct.version>1.6.3</mapstruct.version>
|
||||||
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
|
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
|
||||||
|
|
||||||
<xxl-job.version>2.4.2</xxl-job.version>
|
<xxl-job.version>3.2.0</xxl-job.version>
|
||||||
|
|
||||||
<fastexcel.version>1.1.0</fastexcel.version>
|
<fastexcel.version>1.3.0</fastexcel.version>
|
||||||
|
|
||||||
<!-- 对象存储 -->
|
<!-- 对象存储 -->
|
||||||
<minio.version>8.5.10</minio.version>
|
<minio.version>8.5.10</minio.version>
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
<aliyun-sdk-oss.version>3.16.3</aliyun-sdk-oss.version>
|
<aliyun-sdk-oss.version>3.16.3</aliyun-sdk-oss.version>
|
||||||
|
|
||||||
<!-- redisson 分布式锁 -->
|
<!-- redisson 分布式锁 -->
|
||||||
<redisson.version>3.40.2</redisson.version>
|
<redisson.version>3.51.0</redisson.version>
|
||||||
|
|
||||||
<!-- 自动代码生成 -->
|
<!-- 自动代码生成 -->
|
||||||
<mybatis-plus-generator.version>3.5.6</mybatis-plus-generator.version>
|
<mybatis-plus-generator.version>3.5.6</mybatis-plus-generator.version>
|
||||||
@@ -52,11 +53,11 @@
|
|||||||
<ip2region.version>2.7.0</ip2region.version>
|
<ip2region.version>2.7.0</ip2region.version>
|
||||||
|
|
||||||
<!-- 阿里云短信 -->
|
<!-- 阿里云短信 -->
|
||||||
<aliyun.java.sdk.core.version>4.6.4</aliyun.java.sdk.core.version>
|
<aliyun.java.sdk.core.version>4.7.6</aliyun.java.sdk.core.version>
|
||||||
<aliyun.java.sdk.dysmsapi.version>2.2.1</aliyun.java.sdk.dysmsapi.version>
|
<aliyun.java.sdk.dysmsapi.version>2.2.1</aliyun.java.sdk.dysmsapi.version>
|
||||||
|
|
||||||
<!-- 微信 jdk -->
|
<!-- 微信 jdk -->
|
||||||
<weixin-java.version>4.5.5.B</weixin-java.version>
|
<weixin-java.version>4.7.7.B</weixin-java.version>
|
||||||
<caffeine.version>2.9.3</caffeine.version>
|
<caffeine.version>2.9.3</caffeine.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -152,6 +153,17 @@
|
|||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||||
<version>${knife4j.version}</version>
|
<version>${knife4j.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.8.9</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- MapStruct 对象映射 -->
|
<!-- MapStruct 对象映射 -->
|
||||||
@@ -248,6 +260,13 @@
|
|||||||
<version>${caffeine.version}</version>
|
<version>${caffeine.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 动态多数据源 -->
|
||||||
|
<!--<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||||
|
<version>${dynamic-datasource.version}</version>
|
||||||
|
</dependency>-->
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -260,4 +279,4 @@
|
|||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ INSERT INTO `sys_menu` VALUES (4, 1, '0,1', '菜单管理', 1, 'SysMenu', 'menu'
|
|||||||
INSERT INTO `sys_menu` VALUES (5, 1, '0,1', '部门管理', 1, 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (5, 1, '0,1', '部门管理', 1, 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (6, 1, '0,1', '字典管理', 1, 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (6, 1, '0,1', '字典管理', 1, 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (20, 0, '0', '多级菜单', 2, NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 9, 'cascader', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (20, 0, '0', '多级菜单', 2, NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 9, 'cascader', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (21, 20, '0,20', '菜单一级', 1, NULL, 'multi-level1', 'demo/multi-level/level1', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (21, 20, '0,20', '菜单一级', 2, NULL, 'multi-level1', 'Layout', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (22, 21, '0,20,21', '菜单二级', 1, NULL, 'multi-level2', 'demo/multi-level/children/level2', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (22, 21, '0,20,21', '菜单二级', 2, NULL, 'multi-level2', 'Layout', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (23, 22, '0,20,21,22', '菜单三级-1', 1, NULL, 'multi-level3-1', 'demo/multi-level/children/children/level3-1', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (23, 22, '0,20,21,22', '菜单三级-1', 1, NULL, 'multi-level3-1', 'demo/multi-level/children/children/level3-1', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (24, 22, '0,20,21,22', '菜单三级-2', 1, NULL, 'multi-level3-2', 'demo/multi-level/children/children/level3-2', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (24, 22, '0,20,21,22', '菜单三级-2', 1, NULL, 'multi-level3-2', 'demo/multi-level/children/children/level3-2', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (26, 0, '0', '平台文档', 2, '', '/doc', 'Layout', NULL, NULL, NULL, 1, 8, 'document', 'https://juejin.cn/post/7228990409909108793', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (26, 0, '0', '平台文档', 2, '', '/doc', 'Layout', NULL, NULL, NULL, 1, 8, 'document', 'https://juejin.cn/post/7228990409909108793', now(), now(), NULL);
|
||||||
@@ -153,11 +153,11 @@ INSERT INTO `sys_menu` VALUES (31, 2, '0,1,2', '用户新增', 4, NULL, '', NULL
|
|||||||
INSERT INTO `sys_menu` VALUES (32, 2, '0,1,2', '用户编辑', 4, NULL, '', NULL, 'sys:user:edit', NULL, NULL, 1, 2, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (32, 2, '0,1,2', '用户编辑', 4, NULL, '', NULL, 'sys:user:edit', NULL, NULL, 1, 2, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (33, 2, '0,1,2', '用户删除', 4, NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 3, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (33, 2, '0,1,2', '用户删除', 4, NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 3, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (36, 0, '0', '组件封装', 2, NULL, '/component', 'Layout', NULL, NULL, NULL, 1, 10, 'menu', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (36, 0, '0', '组件封装', 2, NULL, '/component', 'Layout', NULL, NULL, NULL, 1, 10, 'menu', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (37, 36, '0,36', '富文本编辑器', 1, NULL, 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', NULL, NULL, NULL);
|
INSERT INTO `sys_menu` VALUES (37, 36, '0,36', '富文本编辑器', 1, 'WangEditor', 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', NULL, NULL, NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (38, 36, '0,36', '图片上传', 1, NULL, 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (38, 36, '0,36', '图片上传', 1, 'Upload', 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (39, 36, '0,36', '图标选择器', 1, NULL, 'icon-selector', 'demo/icon-selector', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (39, 36, '0,36', '图标选择器', 1, 'IconSelect', 'icon-select', 'demo/icon-select', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (40, 0, '0', '接口文档', 2, NULL, '/api', 'Layout', NULL, 1, NULL, 1, 7, 'api', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (40, 0, '0', '接口文档', 2, NULL, '/api', 'Layout', NULL, 1, NULL, 1, 7, 'api', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (41, 40, '0,40', 'Apifox', 1, NULL, 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (41, 40, '0,40', 'Apifox', 1, 'Apifox', 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (70, 3, '0,1,3', '角色新增', 4, NULL, '', NULL, 'sys:role:add', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (70, 3, '0,1,3', '角色新增', 4, NULL, '', NULL, 'sys:role:add', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (71, 3, '0,1,3', '角色编辑', 4, NULL, '', NULL, 'sys:role:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (71, 3, '0,1,3', '角色编辑', 4, NULL, '', NULL, 'sys:role:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (72, 3, '0,1,3', '角色删除', 4, NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (72, 3, '0,1,3', '角色删除', 4, NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||||
@@ -172,19 +172,18 @@ INSERT INTO `sys_menu` VALUES (81, 6, '0,1,6', '字典编辑', 4, NULL, '', NULL
|
|||||||
INSERT INTO `sys_menu` VALUES (84, 6, '0,1,6', '字典删除', 4, NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (84, 6, '0,1,6', '字典删除', 4, NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (88, 2, '0,1,2', '重置密码', 4, NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (88, 2, '0,1,2', '重置密码', 4, NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (89, 0, '0', '功能演示', 2, NULL, '/function', 'Layout', NULL, NULL, NULL, 1, 12, 'menu', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (89, 0, '0', '功能演示', 2, NULL, '/function', 'Layout', NULL, NULL, NULL, 1, 12, 'menu', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (90, 89, '0,89', 'Websocket', 1, NULL, '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (90, 89, '0,89', 'Websocket', 1, 'WebSocket', '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (91, 89, '0,89', '敬请期待...', 2, NULL, 'other/:id', 'demo/other', NULL, NULL, NULL, 1, 4, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (95, 36, '0,36', '字典组件', 1, 'DictDemo', 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (95, 36, '0,36', '字典组件', 1, NULL, 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (97, 89, '0,89', 'Icons', 1, 'IconDemo', 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (97, 89, '0,89', 'Icons', 1, NULL, 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (102, 26, '0,26', 'document', 3, NULL, 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (102, 26, '0,26', 'document', 3, '', 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
|
|
||||||
INSERT INTO `sys_menu` VALUES (105, 2, '0,1,2', '用户查询', 4, NULL, '', NULL, 'sys:user:query', 0, 0, 1, 0, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (105, 2, '0,1,2', '用户查询', 4, NULL, '', NULL, 'sys:user:query', 0, 0, 1, 0, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (106, 2, '0,1,2', '用户导入', 4, NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (106, 2, '0,1,2', '用户导入', 4, NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (107, 2, '0,1,2', '用户导出', 4, NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (107, 2, '0,1,2', '用户导出', 4, NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (108, 36, '0,36', '增删改查', 1, NULL, 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', NULL, NULL, NULL);
|
INSERT INTO `sys_menu` VALUES (108, 36, '0,36', '增删改查', 1, 'Curd', 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', NULL, NULL, NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (109, 36, '0,36', '列表选择器', 1, NULL, 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', NULL, NULL, NULL);
|
INSERT INTO `sys_menu` VALUES (109, 36, '0,36', '列表选择器', 1, 'TableSelect', 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', NULL, NULL, NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (110, 0, '0', '路由参数', 2, NULL, '/route-param', 'Layout', NULL, 1, 1, 1, 11, 'el-icon-ElementPlus', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (110, 0, '0', '路由参数', 2, NULL, '/route-param', 'Layout', NULL, 1, 1, 1, 11, 'el-icon-ElementPlus', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (111, 110, '0,110', '参数(type=1)', 1, NULL, 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}');
|
INSERT INTO `sys_menu` VALUES (111, 110, '0,110', '参数(type=1)', 1, 'RouteParamType1', 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}');
|
||||||
INSERT INTO `sys_menu` VALUES (112, 110, '0,110', '参数(type=2)', 1, NULL, 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}');
|
INSERT INTO `sys_menu` VALUES (112, 110, '0,110', '参数(type=2)', 1, 'RouteParamType2', 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}');
|
||||||
INSERT INTO `sys_menu` VALUES (117, 1, '0,1', '系统日志', 1, 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 6, 'document', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (117, 1, '0,1', '系统日志', 1, 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 6, 'document', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (118, 0, '0', '系统工具', 2, NULL, '/codegen', 'Layout', NULL, 0, 1, 1, 2, 'menu', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (118, 0, '0', '系统工具', 2, NULL, '/codegen', 'Layout', NULL, 0, 1, 1, 2, 'menu', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (119, 118, '0,118', '代码生成', 1, 'Codegen', 'codegen', 'codegen/index', NULL, 0, 1, 1, 1, 'code', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (119, 118, '0,118', '代码生成', 1, 'Codegen', 'codegen', 'codegen/index', NULL, 0, 1, 1, 1, 'code', NULL, now(), now(), NULL);
|
||||||
@@ -210,10 +209,15 @@ INSERT INTO `sys_menu` VALUES (140, 4, '0,1,4', '菜单查询', 4, NULL, '', NUL
|
|||||||
INSERT INTO `sys_menu` VALUES (141, 5, '0,1,5', '部门查询', 4, NULL, '', NULL, 'sys:dept:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (141, 5, '0,1,5', '部门查询', 4, NULL, '', NULL, 'sys:dept:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (142, 6, '0,1,6', '字典查询', 4, NULL, '', NULL, 'sys:dict:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (142, 6, '0,1,6', '字典查询', 4, NULL, '', NULL, 'sys:dict:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (143, 135, '0,1,135', '字典项查询', 4, NULL, '', NULL, 'sys:dict-item:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
INSERT INTO `sys_menu` VALUES (143, 135, '0,1,135', '字典项查询', 4, NULL, '', NULL, 'sys:dict-item:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (144, 26, '0,26', '后端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 3, 'document', '', '2024-10-05 23:36:03', '2024-10-05 23:36:03', NULL);
|
INSERT INTO `sys_menu` VALUES (144, 26, '0,26', '后端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 3, 'document', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (145, 26, '0,26', '移动端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 4, 'document', '', '2024-10-05 23:36:03', '2024-10-05 23:36:03', NULL);
|
INSERT INTO `sys_menu` VALUES (145, 26, '0,26', '移动端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (146, 36, '0,36', '拖拽组件', 1, NULL, 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', '2025-03-31 14:14:45', '2025-03-31 14:14:52', NULL);
|
INSERT INTO `sys_menu` VALUES (146, 36, '0,36', '拖拽组件', 1, 'Drag', 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', now(), now(), NULL);
|
||||||
INSERT INTO `sys_menu` VALUES (147, 36, '0,36', '滚动文本', 1, NULL, 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', '2025-03-31 14:14:49', '2025-03-31 14:14:56', NULL);
|
INSERT INTO `sys_menu` VALUES (147, 36, '0,36', '滚动文本', 1, 'TextScroll', 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', now(), now(), NULL);
|
||||||
|
INSERT INTO `sys_menu` VALUES (148, 89, '0,89', '字典实时同步', 1, 'DictSync', 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL);
|
||||||
|
INSERT INTO `sys_menu` VALUES (149, 89, '0,89', 'VxeTable', 1, 'VxeTable', 'vxe-table', 'demo/vxe-table/index', NULL, NULL, 1, 1, 0, 'el-icon-MagicStick', '', now(), now(), NULL);
|
||||||
|
INSERT INTO `sys_menu` VALUES (150, 36, '0,36', '自适应表格操作列', 1, 'AutoOperationColumn', 'operation-column', 'demo/auto-operation-column', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL);
|
||||||
|
INSERT INTO `sys_menu` VALUES (151, 89, '0,89', 'CURD单文件', 1, 'CurdSingle', 'curd-single', 'demo/curd-single', NULL, NULL, 1, 1, 7, 'el-icon-Reading', '', now(),now(), NULL);
|
||||||
|
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for sys_role
|
-- Table structure for sys_role
|
||||||
@@ -351,6 +355,10 @@ INSERT INTO `sys_role_menu` VALUES (2, 144);
|
|||||||
INSERT INTO `sys_role_menu` VALUES (2, 145);
|
INSERT INTO `sys_role_menu` VALUES (2, 145);
|
||||||
INSERT INTO `sys_role_menu` VALUES (2, 146);
|
INSERT INTO `sys_role_menu` VALUES (2, 146);
|
||||||
INSERT INTO `sys_role_menu` VALUES (2, 147);
|
INSERT INTO `sys_role_menu` VALUES (2, 147);
|
||||||
|
INSERT INTO `sys_role_menu` VALUES (2, 148);
|
||||||
|
INSERT INTO `sys_role_menu` VALUES (2, 149);
|
||||||
|
INSERT INTO `sys_role_menu` VALUES (2, 150);
|
||||||
|
INSERT INTO `sys_role_menu` VALUES (2, 151);
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for sys_user
|
-- Table structure for sys_user
|
||||||
@@ -374,7 +382,7 @@ CREATE TABLE `sys_user` (
|
|||||||
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
|
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
|
||||||
`openid` char(28) COMMENT '微信 openid',
|
`openid` char(28) COMMENT '微信 openid',
|
||||||
PRIMARY KEY (`id`) USING BTREE,
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
UNIQUE INDEX `login_name`(`username` ASC) USING BTREE
|
KEY `login_name` (`username`)
|
||||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户信息表';
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户信息表';
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
@@ -442,9 +450,11 @@ CREATE TABLE `gen_config` (
|
|||||||
`entity_name` varchar(100) NOT NULL COMMENT '实体类名',
|
`entity_name` varchar(100) NOT NULL COMMENT '实体类名',
|
||||||
`author` varchar(50) NOT NULL COMMENT '作者',
|
`author` varchar(50) NOT NULL COMMENT '作者',
|
||||||
`parent_menu_id` bigint COMMENT '上级菜单ID,对应sys_menu的id ',
|
`parent_menu_id` bigint COMMENT '上级菜单ID,对应sys_menu的id ',
|
||||||
|
`remove_table_prefix` varchar(20) COMMENT '要移除的表前缀,如: sys_',
|
||||||
|
`page_type` varchar(20) COMMENT '页面类型(classic|curd)',
|
||||||
`create_time` datetime COMMENT '创建时间',
|
`create_time` datetime COMMENT '创建时间',
|
||||||
`update_time` datetime COMMENT '更新时间',
|
`update_time` datetime COMMENT '更新时间',
|
||||||
`is_deleted` bit(1) DEFAULT b'0' COMMENT '是否删除',
|
`is_deleted` tinyint(4) DEFAULT 0 COMMENT '是否删除',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_tablename` (`table_name`)
|
UNIQUE KEY `uk_tablename` (`table_name`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成基础配置表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成基础配置表';
|
||||||
@@ -559,4 +569,4 @@ INSERT INTO `sys_user_notice` VALUES (8, 8, 2, 1, NULL, now(), now(), 0);
|
|||||||
INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0);
|
INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0);
|
||||||
INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
|
INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
package com.youlai.boot.shared.auth.controller;
|
package com.youlai.boot.auth.controller;
|
||||||
|
|
||||||
|
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||||
|
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||||
import com.youlai.boot.common.result.Result;
|
import com.youlai.boot.core.web.Result;
|
||||||
import com.youlai.boot.shared.auth.service.AuthService;
|
import com.youlai.boot.auth.service.AuthService;
|
||||||
import com.youlai.boot.shared.auth.model.CaptchaInfo;
|
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||||
import com.youlai.boot.core.security.model.AuthenticationToken;
|
|
||||||
import com.youlai.boot.common.annotation.Log;
|
import com.youlai.boot.common.annotation.Log;
|
||||||
|
import com.youlai.boot.security.model.AuthenticationToken;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证控制层
|
* 认证控制层
|
||||||
*
|
*
|
||||||
@@ -28,10 +32,10 @@ public class AuthController {
|
|||||||
|
|
||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
|
||||||
@Operation(summary = "获取登录验证码")
|
@Operation(summary = "获取验证码")
|
||||||
@GetMapping("/captcha")
|
@GetMapping("/captcha")
|
||||||
public Result<CaptchaInfo> getCaptcha() {
|
public Result<CaptchaVO> getCaptcha() {
|
||||||
CaptchaInfo captcha = authService.getCaptcha();
|
CaptchaVO captcha = authService.getCaptcha();
|
||||||
return Result.success(captcha);
|
return Result.success(captcha);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,42 +50,6 @@ public class AuthController {
|
|||||||
return Result.success(authenticationToken);
|
return Result.success(authenticationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "注销登录")
|
|
||||||
@DeleteMapping("/logout")
|
|
||||||
@Log(value = "注销", module = LogModuleEnum.LOGIN)
|
|
||||||
public Result<?> logout() {
|
|
||||||
authService.logout();
|
|
||||||
return Result.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "刷新访问令牌")
|
|
||||||
@PostMapping("/refresh-token")
|
|
||||||
public Result<?> refreshToken(
|
|
||||||
@Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken
|
|
||||||
) {
|
|
||||||
AuthenticationToken authenticationToken = authService.refreshToken(refreshToken);
|
|
||||||
return Result.success(authenticationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "微信授权登录")
|
|
||||||
@PostMapping("/login/wechat")
|
|
||||||
@Log(value = "微信登录", module = LogModuleEnum.LOGIN)
|
|
||||||
public Result<AuthenticationToken> loginByWechat(
|
|
||||||
@Parameter(description = "微信授权码", example = "code") @RequestParam String code
|
|
||||||
) {
|
|
||||||
AuthenticationToken loginResult = authService.loginByWechat(code);
|
|
||||||
return Result.success(loginResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "发送登录短信验证码")
|
|
||||||
@PostMapping("/login/sms/code")
|
|
||||||
public Result<Void> sendLoginVerifyCode(
|
|
||||||
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile
|
|
||||||
) {
|
|
||||||
authService.sendSmsLoginCode(mobile);
|
|
||||||
return Result.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "短信验证码登录")
|
@Operation(summary = "短信验证码登录")
|
||||||
@PostMapping("/login/sms")
|
@PostMapping("/login/sms")
|
||||||
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
|
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
|
||||||
@@ -92,4 +60,56 @@ public class AuthController {
|
|||||||
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
|
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
|
||||||
return Result.success(loginResult);
|
return Result.success(loginResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "发送登录短信验证码")
|
||||||
|
@PostMapping("/sms/code")
|
||||||
|
public Result<Void> sendLoginVerifyCode(
|
||||||
|
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile
|
||||||
|
) {
|
||||||
|
authService.sendSmsLoginCode(mobile);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "微信授权登录(Web)")
|
||||||
|
@PostMapping("/login/wechat")
|
||||||
|
@Log(value = "微信登录", module = LogModuleEnum.LOGIN)
|
||||||
|
public Result<AuthenticationToken> loginByWechat(
|
||||||
|
@Parameter(description = "微信授权码", example = "code") @RequestParam String code
|
||||||
|
) {
|
||||||
|
AuthenticationToken loginResult = authService.loginByWechat(code);
|
||||||
|
return Result.success(loginResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "微信小程序登录(Code)")
|
||||||
|
@PostMapping("/wx/miniapp/code-login")
|
||||||
|
public Result<AuthenticationToken> loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDTO loginDTO) {
|
||||||
|
AuthenticationToken token = authService.loginByWxMiniAppCode(loginDTO);
|
||||||
|
return Result.success(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "微信小程序登录(手机号)")
|
||||||
|
@PostMapping("/wx/miniapp/phone-login")
|
||||||
|
public Result<AuthenticationToken> loginByWxMiniAppPhone(@RequestBody @Valid WxMiniAppPhoneLoginDTO loginDTO) {
|
||||||
|
AuthenticationToken token = authService.loginByWxMiniAppPhone(loginDTO);
|
||||||
|
return Result.success(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Operation(summary = "退出登录")
|
||||||
|
@DeleteMapping("/logout")
|
||||||
|
@Log(value = "退出登录", module = LogModuleEnum.LOGIN)
|
||||||
|
public Result<?> logout() {
|
||||||
|
authService.logout();
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "刷新令牌")
|
||||||
|
@PostMapping("/refresh-token")
|
||||||
|
public Result<?> refreshToken(
|
||||||
|
@Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken
|
||||||
|
) {
|
||||||
|
AuthenticationToken authenticationToken = authService.refreshToken(refreshToken);
|
||||||
|
return Result.success(authenticationToken);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.youlai.boot.auth.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序Code登录请求参数
|
||||||
|
*
|
||||||
|
* @author 有来技术团队
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Schema(description = "微信小程序Code登录请求参数")
|
||||||
|
@Data
|
||||||
|
public class WxMiniAppCodeLoginDTO {
|
||||||
|
|
||||||
|
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
@NotBlank(message = "code不能为空")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.youlai.boot.auth.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序手机号登录请求参数
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Schema(description = "微信小程序手机号登录请求参数")
|
||||||
|
@Data
|
||||||
|
public class WxMiniAppPhoneLoginDTO {
|
||||||
|
|
||||||
|
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
@NotBlank(message = "code不能为空")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Schema(description = "包括敏感数据在内的完整用户信息的加密数据")
|
||||||
|
private String encryptedData;
|
||||||
|
|
||||||
|
@Schema(description = "加密算法的初始向量")
|
||||||
|
private String iv;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.auth.model;
|
package com.youlai.boot.auth.model.vo;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -13,7 +13,7 @@ import lombok.Data;
|
|||||||
@Schema(description = "验证码信息")
|
@Schema(description = "验证码信息")
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
public class CaptchaInfo {
|
public class CaptchaVO {
|
||||||
|
|
||||||
@Schema(description = "验证码缓存 Key")
|
@Schema(description = "验证码缓存 Key")
|
||||||
private String captchaKey;
|
private String captchaKey;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.youlai.boot.shared.auth.service;
|
package com.youlai.boot.auth.service;
|
||||||
|
|
||||||
import com.youlai.boot.shared.auth.model.CaptchaInfo;
|
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||||
import com.youlai.boot.core.security.model.AuthenticationToken;
|
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||||
|
import com.youlai.boot.security.model.AuthenticationToken;
|
||||||
|
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证服务接口
|
* 认证服务接口
|
||||||
@@ -30,7 +32,7 @@ public interface AuthService {
|
|||||||
*
|
*
|
||||||
* @return 验证码
|
* @return 验证码
|
||||||
*/
|
*/
|
||||||
CaptchaInfo getCaptcha();
|
CaptchaVO getCaptcha();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新令牌
|
* 刷新令牌
|
||||||
@@ -48,6 +50,22 @@ public interface AuthService {
|
|||||||
*/
|
*/
|
||||||
AuthenticationToken loginByWechat(String code);
|
AuthenticationToken loginByWechat(String code);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序Code登录
|
||||||
|
*
|
||||||
|
* @param loginDTO 登录参数
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序手机号登录
|
||||||
|
*
|
||||||
|
* @param loginDTO 登录参数
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送短信验证码
|
* 发送短信验证码
|
||||||
*
|
*
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
package com.youlai.boot.shared.auth.service.impl;
|
package com.youlai.boot.auth.service.impl;
|
||||||
|
|
||||||
import cn.hutool.captcha.AbstractCaptcha;
|
import cn.hutool.captcha.AbstractCaptcha;
|
||||||
import cn.hutool.captcha.CaptchaUtil;
|
import cn.hutool.captcha.CaptchaUtil;
|
||||||
import cn.hutool.captcha.generator.CodeGenerator;
|
import cn.hutool.captcha.generator.CodeGenerator;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||||
|
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||||
|
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||||
|
import com.youlai.boot.auth.service.AuthService;
|
||||||
import com.youlai.boot.common.constant.RedisConstants;
|
import com.youlai.boot.common.constant.RedisConstants;
|
||||||
import com.youlai.boot.common.constant.SecurityConstants;
|
import com.youlai.boot.common.constant.SecurityConstants;
|
||||||
import com.youlai.boot.common.exception.BusinessException;
|
import com.youlai.boot.common.enums.CaptchaTypeEnum;
|
||||||
import com.youlai.boot.common.result.ResultCode;
|
|
||||||
import com.youlai.boot.config.property.CaptchaProperties;
|
import com.youlai.boot.config.property.CaptchaProperties;
|
||||||
import com.youlai.boot.core.security.extension.sms.SmsAuthenticationToken;
|
import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||||
import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationToken;
|
import com.youlai.boot.platform.sms.service.SmsService;
|
||||||
import com.youlai.boot.core.security.util.SecurityUtils;
|
import com.youlai.boot.security.model.AuthenticationToken;
|
||||||
import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum;
|
import com.youlai.boot.security.model.SmsAuthenticationToken;
|
||||||
import com.youlai.boot.core.security.model.AuthenticationToken;
|
import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken;
|
||||||
import com.youlai.boot.shared.auth.model.CaptchaInfo;
|
import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken;
|
||||||
import com.youlai.boot.shared.auth.service.AuthService;
|
import com.youlai.boot.security.token.TokenManager;
|
||||||
import com.youlai.boot.core.security.token.TokenManager;
|
import com.youlai.boot.security.util.SecurityUtils;
|
||||||
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
|
|
||||||
import com.youlai.boot.shared.sms.service.SmsService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
@@ -87,16 +88,16 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
@Override
|
@Override
|
||||||
public AuthenticationToken loginByWechat(String code) {
|
public AuthenticationToken loginByWechat(String code) {
|
||||||
// 1. 创建用户微信认证的令牌(未认证)
|
// 1. 创建用户微信认证的令牌(未认证)
|
||||||
WechatAuthenticationToken wechatAuthenticationToken = new WechatAuthenticationToken(code);
|
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(code);
|
||||||
|
|
||||||
// 2. 执行认证(认证中)
|
// 2. 执行认证(认证中)
|
||||||
Authentication authentication = authenticationManager.authenticate(wechatAuthenticationToken);
|
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||||
|
|
||||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||||
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
|
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
return authenticationToken;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,7 +168,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
* @return 验证码
|
* @return 验证码
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public CaptchaInfo getCaptcha() {
|
public CaptchaVO getCaptcha() {
|
||||||
|
|
||||||
String captchaType = captchaProperties.getType();
|
String captchaType = captchaProperties.getType();
|
||||||
int width = captchaProperties.getWidth();
|
int width = captchaProperties.getWidth();
|
||||||
@@ -203,7 +204,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
TimeUnit.SECONDS
|
TimeUnit.SECONDS
|
||||||
);
|
);
|
||||||
|
|
||||||
return CaptchaInfo.builder()
|
return CaptchaVO.builder()
|
||||||
.captchaKey(captchaKey)
|
.captchaKey(captchaKey)
|
||||||
.captchaBase64(imageBase64Data)
|
.captchaBase64(imageBase64Data)
|
||||||
.build();
|
.build();
|
||||||
@@ -217,15 +218,53 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationToken refreshToken(String refreshToken) {
|
public AuthenticationToken refreshToken(String refreshToken) {
|
||||||
// 验证刷新令牌
|
|
||||||
boolean isValidate = tokenManager.validateRefreshToken(refreshToken);
|
|
||||||
|
|
||||||
if (!isValidate) {
|
|
||||||
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
|
|
||||||
}
|
|
||||||
// 刷新令牌有效,生成新的访问令牌
|
|
||||||
return tokenManager.refreshToken(refreshToken);
|
return tokenManager.refreshToken(refreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序Code登录
|
||||||
|
*
|
||||||
|
* @param loginDTO 登录参数
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO) {
|
||||||
|
// 1. 创建微信小程序认证令牌(未认证)
|
||||||
|
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(loginDTO.getCode());
|
||||||
|
|
||||||
|
// 2. 执行认证(认证中)
|
||||||
|
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||||
|
|
||||||
|
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||||
|
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序手机号登录
|
||||||
|
*
|
||||||
|
* @param loginDTO 登录参数
|
||||||
|
* @return 访问令牌
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO) {
|
||||||
|
// 创建微信小程序手机号认证Token
|
||||||
|
WxMiniAppPhoneAuthenticationToken authenticationToken = new WxMiniAppPhoneAuthenticationToken(
|
||||||
|
loginDTO.getCode(),
|
||||||
|
loginDTO.getEncryptedData(),
|
||||||
|
loginDTO.getIv()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 执行认证
|
||||||
|
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||||
|
|
||||||
|
// 认证成功后生成JWT令牌,并存入Security上下文
|
||||||
|
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,11 @@ package com.youlai.boot.common.constant;
|
|||||||
*/
|
*/
|
||||||
public interface JwtClaimConstants {
|
public interface JwtClaimConstants {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 令牌类型
|
||||||
|
*/
|
||||||
|
String TOKEN_TYPE = "tokenType";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户ID
|
* 用户ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.auth.enums;
|
package com.youlai.boot.common.enums;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EasyCaptcha 验证码类型枚举
|
* EasyCaptcha 验证码类型枚举
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package com.youlai.boot.common.util;
|
|
||||||
|
|
||||||
import cn.hutool.json.JSONUtil;
|
|
||||||
import com.youlai.boot.common.result.Result;
|
|
||||||
import com.youlai.boot.common.result.ResultCode;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 响应工具类
|
|
||||||
*
|
|
||||||
* @author Ray.Hao
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class ResponseUtils {
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 异常消息返回(适用过滤器中处理异常响应)
|
|
||||||
*
|
|
||||||
* @param response HttpServletResponse
|
|
||||||
* @param resultCode 响应结果码
|
|
||||||
*/
|
|
||||||
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
|
|
||||||
int status = getHttpStatus(resultCode);
|
|
||||||
|
|
||||||
response.setStatus(status);
|
|
||||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
|
||||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
|
||||||
|
|
||||||
try (PrintWriter writer = response.getWriter()) {
|
|
||||||
String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));
|
|
||||||
writer.print(jsonResponse);
|
|
||||||
writer.flush(); // 确保将响应内容写入到输出流
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("响应异常处理失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 异常消息返回(适用过滤器中处理异常响应)
|
|
||||||
*
|
|
||||||
* @param response HttpServletResponse
|
|
||||||
* @param resultCode 响应结果码
|
|
||||||
*/
|
|
||||||
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) {
|
|
||||||
int status = getHttpStatus(resultCode);
|
|
||||||
|
|
||||||
response.setStatus(status);
|
|
||||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
|
||||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
|
||||||
|
|
||||||
try (PrintWriter writer = response.getWriter()) {
|
|
||||||
String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message));
|
|
||||||
writer.print(jsonResponse);
|
|
||||||
writer.flush(); // 确保将响应内容写入到输出流
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("响应异常处理失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据结果码获取HTTP状态码
|
|
||||||
*
|
|
||||||
* @param resultCode 结果码
|
|
||||||
* @return HTTP状态码
|
|
||||||
*/
|
|
||||||
private static int getHttpStatus(ResultCode resultCode) {
|
|
||||||
return switch (resultCode) {
|
|
||||||
case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
|
|
||||||
default -> HttpStatus.BAD_REQUEST.value();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
import com.youlai.boot.core.handler.MyDataPermissionHandler;
|
import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler;
|
||||||
import com.youlai.boot.core.handler.MyMetaObjectHandler;
|
import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import cn.hutool.captcha.generator.CodeGenerator;
|
|||||||
import cn.hutool.core.util.ArrayUtil;
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import com.youlai.boot.config.property.SecurityProperties;
|
import com.youlai.boot.config.property.SecurityProperties;
|
||||||
import com.youlai.boot.core.filter.RateLimiterFilter;
|
import com.youlai.boot.core.filter.RateLimiterFilter;
|
||||||
import com.youlai.boot.core.security.exception.MyAccessDeniedHandler;
|
import com.youlai.boot.security.filter.CaptchaValidationFilter;
|
||||||
import com.youlai.boot.core.security.exception.MyAuthenticationEntryPoint;
|
import com.youlai.boot.security.filter.TokenAuthenticationFilter;
|
||||||
import com.youlai.boot.core.security.extension.sms.SmsAuthenticationProvider;
|
import com.youlai.boot.security.handler.MyAccessDeniedHandler;
|
||||||
import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationProvider;
|
import com.youlai.boot.security.handler.MyAuthenticationEntryPoint;
|
||||||
import com.youlai.boot.core.security.filter.CaptchaValidationFilter;
|
import com.youlai.boot.security.provider.SmsAuthenticationProvider;
|
||||||
import com.youlai.boot.core.security.filter.TokenAuthenticationFilter;
|
import com.youlai.boot.security.provider.WxMiniAppCodeAuthenticationProvider;
|
||||||
import com.youlai.boot.core.security.token.TokenManager;
|
import com.youlai.boot.security.provider.WxMiniAppPhoneAuthenticationProvider;
|
||||||
import com.youlai.boot.core.security.service.SysUserDetailsService;
|
import com.youlai.boot.security.token.TokenManager;
|
||||||
|
import com.youlai.boot.security.service.SysUserDetailsService;
|
||||||
import com.youlai.boot.system.service.ConfigService;
|
import com.youlai.boot.system.service.ConfigService;
|
||||||
import com.youlai.boot.system.service.UserService;
|
import com.youlai.boot.system.service.UserService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -118,20 +119,26 @@ public class SecurityConfig {
|
|||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
|
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService);
|
||||||
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
|
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
|
||||||
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
|
|
||||||
return daoAuthenticationProvider;
|
return daoAuthenticationProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 微信认证 Provider
|
* 微信小程序Code认证Provider
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public WechatAuthenticationProvider weChatAuthenticationProvider() {
|
public WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider() {
|
||||||
return new WechatAuthenticationProvider(userService, wxMaService);
|
return new WxMiniAppCodeAuthenticationProvider(userService, wxMaService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序手机号认证Provider
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider() {
|
||||||
|
return new WxMiniAppPhoneAuthenticationProvider(userService, wxMaService);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 短信验证码认证 Provider
|
* 短信验证码认证 Provider
|
||||||
@@ -147,12 +154,14 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public AuthenticationManager authenticationManager(
|
public AuthenticationManager authenticationManager(
|
||||||
DaoAuthenticationProvider daoAuthenticationProvider,
|
DaoAuthenticationProvider daoAuthenticationProvider,
|
||||||
WechatAuthenticationProvider weChatAuthenticationProvider,
|
WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider,
|
||||||
|
WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider,
|
||||||
SmsAuthenticationProvider smsAuthenticationProvider
|
SmsAuthenticationProvider smsAuthenticationProvider
|
||||||
) {
|
) {
|
||||||
return new ProviderManager(
|
return new ProviderManager(
|
||||||
daoAuthenticationProvider,
|
daoAuthenticationProvider,
|
||||||
weChatAuthenticationProvider,
|
wxMiniAppCodeAuthenticationProvider,
|
||||||
|
wxMiniAppPhoneAuthenticationProvider,
|
||||||
smsAuthenticationProvider
|
smsAuthenticationProvider
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
package com.youlai.boot.config;
|
package com.youlai.boot.config;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
<<<<<<< HEAD
|
||||||
import com.youlai.boot.core.security.model.SysUserDetails;
|
import com.youlai.boot.core.security.model.SysUserDetails;
|
||||||
import com.youlai.boot.core.security.token.TokenManager;
|
import com.youlai.boot.core.security.token.TokenManager;
|
||||||
import com.youlai.boot.system.service.UserOnlineService;
|
import com.youlai.boot.system.service.UserOnlineService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
=======
|
||||||
|
import com.youlai.boot.security.model.SysUserDetails;
|
||||||
|
import com.youlai.boot.security.token.TokenManager;
|
||||||
|
import com.youlai.boot.system.service.WebSocketService;
|
||||||
|
>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.MessageChannel;
|
import org.springframework.messaging.MessageChannel;
|
||||||
@@ -27,9 +34,21 @@ import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
|||||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
<<<<<<< HEAD
|
||||||
* WebSocket配置
|
* WebSocket配置
|
||||||
*
|
*
|
||||||
* @author You Lai
|
* @author You Lai
|
||||||
|
=======
|
||||||
|
* WebSocket 配置类
|
||||||
|
*
|
||||||
|
* 核心功能:
|
||||||
|
* - 配置 WebSocket 端点
|
||||||
|
* - 配置消息代理
|
||||||
|
* - 实现连接认证与授权
|
||||||
|
* - 管理用户会话生命周期
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
@EnableWebSocketMessageBroker
|
@EnableWebSocketMessageBroker
|
||||||
@@ -38,15 +57,34 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
private final TokenManager tokenManager;
|
private final TokenManager tokenManager;
|
||||||
private final UserOnlineService userOnlineService;
|
private final UserOnlineService userOnlineService;
|
||||||
|
=======
|
||||||
|
private static final String WS_ENDPOINT = "/ws";
|
||||||
|
private static final String APP_DESTINATION_PREFIX = "/app";
|
||||||
|
private static final String USER_DESTINATION_PREFIX = "/user";
|
||||||
|
private static final String[] BROKER_DESTINATIONS = {"/topic", "/queue"};
|
||||||
|
|
||||||
|
private final TokenManager tokenManager;
|
||||||
|
private final WebSocketService webSocketService;
|
||||||
|
|
||||||
|
public WebSocketConfig(TokenManager tokenManager, @Lazy WebSocketService webSocketService) {
|
||||||
|
this.tokenManager = tokenManager;
|
||||||
|
this.webSocketService = webSocketService;
|
||||||
|
log.info("✓ WebSocket 配置已加载");
|
||||||
|
}
|
||||||
|
>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册一个端点,客户端通过这个端点进行连接
|
* 注册 STOMP 端点
|
||||||
|
*
|
||||||
|
* 客户端通过该端点建立 WebSocket 连接
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
registry
|
registry
|
||||||
|
<<<<<<< HEAD
|
||||||
// 注册 /ws 的端点
|
// 注册 /ws 的端点
|
||||||
.addEndpoint("/ws")
|
.addEndpoint("/ws")
|
||||||
// 允许跨域
|
// 允许跨域
|
||||||
@@ -54,31 +92,53 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
// 开启SockJS支持,用于不支持WebSocket的浏览器
|
// 开启SockJS支持,用于不支持WebSocket的浏览器
|
||||||
.withSockJS();
|
.withSockJS();
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
.addEndpoint(WS_ENDPOINT)
|
||||||
|
.setAllowedOriginPatterns("*"); // 允许跨域(生产环境建议配置具体域名)
|
||||||
|
>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d
|
||||||
|
|
||||||
|
log.info("✓ STOMP 端点已注册: {}", WS_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置消息代理
|
* 配置消息代理
|
||||||
|
*
|
||||||
|
* - /app 前缀:客户端发送消息到服务端的前缀
|
||||||
|
* - /topic 前缀:用于广播消息
|
||||||
|
* - /queue 前缀:用于点对点消息
|
||||||
|
* - /user 前缀:服务端发送给特定用户的消息前缀
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||||
// 客户端发送消息的请求前缀
|
// 客户端发送消息的请求前缀
|
||||||
registry.setApplicationDestinationPrefixes("/app");
|
registry.setApplicationDestinationPrefixes(APP_DESTINATION_PREFIX);
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
|
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
|
||||||
registry.enableSimpleBroker("/topic", "/queue");
|
registry.enableSimpleBroker("/topic", "/queue");
|
||||||
|
|
||||||
// 服务端通知客户端的前缀,可以不设置,默认为user
|
// 服务端通知客户端的前缀,可以不设置,默认为user
|
||||||
registry.setUserDestinationPrefix("/user");
|
registry.setUserDestinationPrefix("/user");
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
// 启用简单消息代理,处理 /topic 和 /queue 前缀的消息
|
||||||
|
registry.enableSimpleBroker(BROKER_DESTINATIONS);
|
||||||
|
|
||||||
|
// 服务端通知客户端的前缀
|
||||||
|
registry.setUserDestinationPrefix(USER_DESTINATION_PREFIX);
|
||||||
|
>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d
|
||||||
|
|
||||||
|
log.info("✓ 消息代理已配置: app={}, broker={}, user={}",
|
||||||
|
APP_DESTINATION_PREFIX, BROKER_DESTINATIONS, USER_DESTINATION_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置客户端入站通道拦截器
|
* 配置客户端入站通道拦截器
|
||||||
* <p>
|
*
|
||||||
* 核心功能:
|
* 核心功能:
|
||||||
* 1. 连接建立时解析令牌并绑定用户身份
|
* 1. 连接建立时:解析 JWT Token 并绑定用户身份
|
||||||
* 2. 连接关闭时触发下线通知
|
* 2. 连接关闭时:触发用户下线通知
|
||||||
* 3. 异常Token的防御性处理
|
* 3. 安全防护:拦截无效连接请求
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||||
@@ -86,11 +146,20 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
@Override
|
@Override
|
||||||
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
|
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
|
||||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||||
|
|
||||||
|
// 防御性检查:确保 accessor 不为空
|
||||||
if (accessor == null) {
|
if (accessor == null) {
|
||||||
|
log.warn("⚠ 收到异常消息:无法获取 StompHeaderAccessor");
|
||||||
|
return ChannelInterceptor.super.preSend(message, channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
StompCommand command = accessor.getCommand();
|
||||||
|
if (command == null) {
|
||||||
return ChannelInterceptor.super.preSend(message, channel);
|
return ChannelInterceptor.super.preSend(message, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
<<<<<<< HEAD
|
||||||
// 处理客户端连接请求
|
// 处理客户端连接请求
|
||||||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||||
/*
|
/*
|
||||||
@@ -149,19 +218,178 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
// 记录用户下线状态
|
// 记录用户下线状态
|
||||||
userOnlineService.userDisconnected(username);
|
userOnlineService.userDisconnected(username);
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
switch (command) {
|
||||||
|
case CONNECT:
|
||||||
|
handleConnect(accessor);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DISCONNECT:
|
||||||
|
handleDisconnect(accessor);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SUBSCRIBE:
|
||||||
|
handleSubscribe(accessor);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 其他命令不需要特殊处理
|
||||||
|
break;
|
||||||
|
>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d
|
||||||
}
|
}
|
||||||
} catch (AuthenticationException ex) {
|
} catch (AuthenticationException ex) {
|
||||||
// 认证失败时强制关闭连接
|
// 认证失败时强制关闭连接
|
||||||
log.error("连接认证失败:{}", ex.getMessage());
|
log.error("❌ 连接认证失败: {}", ex.getMessage());
|
||||||
throw ex;
|
throw ex;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// 捕获其他未知异常
|
// 捕获其他未知异常
|
||||||
log.error("WebSocket连接处理异常:", ex);
|
log.error("❌ WebSocket 消息处理异常", ex);
|
||||||
throw new MessagingException("Connection processing failed");
|
throw new MessagingException("消息处理失败: " + ex.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChannelInterceptor.super.preSend(message, channel);
|
return ChannelInterceptor.super.preSend(message, channel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.info("✓ 客户端入站通道拦截器已配置");
|
||||||
}
|
}
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理客户端连接请求
|
||||||
|
*
|
||||||
|
* 安全校验流程:
|
||||||
|
* 1. 提取 Authorization 头
|
||||||
|
* 2. 验证 Bearer Token 格式
|
||||||
|
* 3. 解析并验证 JWT 有效性
|
||||||
|
* 4. 绑定用户身份到当前会话
|
||||||
|
* 5. 记录用户上线状态
|
||||||
|
*/
|
||||||
|
private void handleConnect(StompHeaderAccessor accessor) {
|
||||||
|
String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
|
// 安全检查:确保 Authorization 头存在且格式正确
|
||||||
|
if (StrUtil.isBlank(authorization)) {
|
||||||
|
log.warn("⚠ 非法连接请求:缺少 Authorization 头");
|
||||||
|
throw new AuthenticationCredentialsNotFoundException("缺少 Authorization 头");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authorization.startsWith("Bearer ")) {
|
||||||
|
log.warn("⚠ 非法连接请求:Authorization 头格式错误");
|
||||||
|
throw new BadCredentialsException("Authorization 头格式错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 JWT Token(移除 "Bearer " 前缀)
|
||||||
|
String token = authorization.substring(7);
|
||||||
|
|
||||||
|
if (StrUtil.isBlank(token)) {
|
||||||
|
log.warn("⚠ 非法连接请求:Token 为空");
|
||||||
|
throw new BadCredentialsException("Token 为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析并验证 Token
|
||||||
|
Authentication authentication;
|
||||||
|
try {
|
||||||
|
authentication = tokenManager.parseToken(token);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("❌ Token 解析失败", ex);
|
||||||
|
throw new BadCredentialsException("Token 无效: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证解析结果
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
log.warn("⚠ Token 解析失败:认证对象无效");
|
||||||
|
throw new BadCredentialsException("Token 解析失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户详细信息
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
if (!(principal instanceof SysUserDetails)) {
|
||||||
|
log.error("❌ 无效的用户凭证类型: {}", principal.getClass().getName());
|
||||||
|
throw new BadCredentialsException("用户凭证类型错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
SysUserDetails userDetails = (SysUserDetails) principal;
|
||||||
|
String username = userDetails.getUsername();
|
||||||
|
|
||||||
|
if (StrUtil.isBlank(username)) {
|
||||||
|
log.warn("⚠ 用户名为空");
|
||||||
|
throw new BadCredentialsException("用户名为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定用户身份到当前会话(重要:用于 @SendToUser 等注解)
|
||||||
|
accessor.setUser(authentication);
|
||||||
|
|
||||||
|
// 获取会话 ID
|
||||||
|
String sessionId = accessor.getSessionId();
|
||||||
|
if (sessionId == null) {
|
||||||
|
log.warn("⚠ 会话 ID 为空,使用临时 ID");
|
||||||
|
sessionId = "temp-" + System.nanoTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录用户上线状态
|
||||||
|
try {
|
||||||
|
webSocketService.userConnected(username, sessionId);
|
||||||
|
log.info("✓ WebSocket 连接建立成功: 用户[{}], 会话[{}]", username, sessionId);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("❌ 记录用户上线状态失败: 用户[{}], 会话[{}]", username, sessionId, ex);
|
||||||
|
// 不抛出异常,允许连接继续
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理客户端断开连接事件
|
||||||
|
*
|
||||||
|
* 注意:
|
||||||
|
* - 只有成功建立过认证的连接才会触发下线事件
|
||||||
|
* - 防止未认证成功的连接产生脏数据
|
||||||
|
*/
|
||||||
|
private void handleDisconnect(StompHeaderAccessor accessor) {
|
||||||
|
Authentication authentication = (Authentication) accessor.getUser();
|
||||||
|
|
||||||
|
// 防御性检查:只处理已认证的连接
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
log.debug("未认证的连接断开,跳过处理");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
if (!(principal instanceof SysUserDetails)) {
|
||||||
|
log.warn("⚠ 断开连接时用户凭证类型异常");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SysUserDetails userDetails = (SysUserDetails) principal;
|
||||||
|
String username = userDetails.getUsername();
|
||||||
|
|
||||||
|
if (StrUtil.isNotBlank(username)) {
|
||||||
|
try {
|
||||||
|
webSocketService.userDisconnected(username);
|
||||||
|
log.info("✓ WebSocket 连接断开: 用户[{}]", username);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("❌ 记录用户下线状态失败: 用户[{}]", username, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理客户端订阅事件(可选)
|
||||||
|
*
|
||||||
|
* 用于记录订阅信息或实施订阅级别的权限控制
|
||||||
|
*/
|
||||||
|
private void handleSubscribe(StompHeaderAccessor accessor) {
|
||||||
|
Authentication authentication = (Authentication) accessor.getUser();
|
||||||
|
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
String destination = accessor.getDestination();
|
||||||
|
String username = authentication.getName();
|
||||||
|
|
||||||
|
log.debug("用户[{}]订阅主题: {}", username, destination);
|
||||||
|
|
||||||
|
// TODO: 这里可以实现订阅级别的权限控制
|
||||||
|
// 例如:检查用户是否有权限订阅某个主题
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import cn.hutool.json.JSONUtil;
|
|||||||
import com.aliyun.oss.HttpMethod;
|
import com.aliyun.oss.HttpMethod;
|
||||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||||
import com.youlai.boot.common.util.IPUtils;
|
import com.youlai.boot.common.util.IPUtils;
|
||||||
import com.youlai.boot.core.security.util.SecurityUtils;
|
import com.youlai.boot.security.util.SecurityUtils;
|
||||||
import com.youlai.boot.system.model.entity.Log;
|
import com.youlai.boot.system.model.entity.Log;
|
||||||
import com.youlai.boot.system.service.LogService;
|
import com.youlai.boot.system.service.LogService;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -60,6 +60,9 @@ public class LogAspect {
|
|||||||
*/
|
*/
|
||||||
@Around("logPointcut() && @annotation(logAnnotation)")
|
@Around("logPointcut() && @annotation(logAnnotation)")
|
||||||
public Object doAround(ProceedingJoinPoint joinPoint, com.youlai.boot.common.annotation.Log logAnnotation) throws Throwable {
|
public Object doAround(ProceedingJoinPoint joinPoint, com.youlai.boot.common.annotation.Log logAnnotation) throws Throwable {
|
||||||
|
// 在方法执行前获取用户ID,避免在方法执行过程中清除上下文导致获取不到用户ID
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
|
||||||
TimeInterval timer = DateUtil.timer();
|
TimeInterval timer = DateUtil.timer();
|
||||||
Object result = null;
|
Object result = null;
|
||||||
Exception exception = null;
|
Exception exception = null;
|
||||||
@@ -71,7 +74,7 @@ public class LogAspect {
|
|||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
long executionTime = timer.interval(); // 执行时长
|
long executionTime = timer.interval(); // 执行时长
|
||||||
this.saveLog(joinPoint, exception, result, logAnnotation, executionTime);
|
this.saveLog(joinPoint, exception, result, logAnnotation, executionTime, userId);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -84,8 +87,9 @@ public class LogAspect {
|
|||||||
* @param e 异常
|
* @param e 异常
|
||||||
* @param jsonResult 响应结果
|
* @param jsonResult 响应结果
|
||||||
* @param logAnnotation 日志注解
|
* @param logAnnotation 日志注解
|
||||||
|
* @param userId 用户ID
|
||||||
*/
|
*/
|
||||||
private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, com.youlai.boot.common.annotation.Log logAnnotation, long executionTime) {
|
private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, com.youlai.boot.common.annotation.Log logAnnotation, long executionTime, Long userId) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
// 创建日志记录
|
// 创建日志记录
|
||||||
Log log = new Log();
|
Log log = new Log();
|
||||||
@@ -108,7 +112,6 @@ public class LogAspect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.setRequestUri(requestURI);
|
log.setRequestUri(requestURI);
|
||||||
Long userId = SecurityUtils.getUserId();
|
|
||||||
log.setCreateBy(userId);
|
log.setCreateBy(userId);
|
||||||
String ipAddr = IPUtils.getIpAddr(request);
|
String ipAddr = IPUtils.getIpAddr(request);
|
||||||
if (StrUtil.isNotBlank(ipAddr)) {
|
if (StrUtil.isNotBlank(ipAddr)) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import cn.hutool.core.util.StrUtil;
|
|||||||
import cn.hutool.crypto.digest.DigestUtil;
|
import cn.hutool.crypto.digest.DigestUtil;
|
||||||
import com.youlai.boot.common.constant.RedisConstants;
|
import com.youlai.boot.common.constant.RedisConstants;
|
||||||
import com.youlai.boot.common.constant.SecurityConstants;
|
import com.youlai.boot.common.constant.SecurityConstants;
|
||||||
import com.youlai.boot.common.result.ResultCode;
|
import com.youlai.boot.core.web.ResultCode;
|
||||||
import com.youlai.boot.common.exception.BusinessException;
|
import com.youlai.boot.core.exception.BusinessException;
|
||||||
import com.youlai.boot.common.annotation.RepeatSubmit;
|
import com.youlai.boot.common.annotation.RepeatSubmit;
|
||||||
import com.youlai.boot.common.util.IPUtils;
|
import com.youlai.boot.common.util.IPUtils;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.youlai.boot.common.exception;
|
package com.youlai.boot.core.exception;
|
||||||
|
|
||||||
import com.youlai.boot.common.result.IResultCode;
|
import com.youlai.boot.core.web.IResultCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.slf4j.helpers.MessageFormatter;
|
import org.slf4j.helpers.MessageFormatter;
|
||||||
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.youlai.boot.common.exception;
|
package com.youlai.boot.core.exception;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.youlai.boot.common.result.Result;
|
import com.youlai.boot.core.web.Result;
|
||||||
import com.youlai.boot.common.result.ResultCode;
|
import com.youlai.boot.core.web.ResultCode;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.TypeMismatchException;
|
import org.springframework.beans.TypeMismatchException;
|
||||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||||
@@ -21,10 +24,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|||||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import java.sql.SQLIntegrityConstraintViolationException;
|
||||||
import jakarta.validation.ConstraintViolation;
|
|
||||||
import jakarta.validation.ConstraintViolationException;
|
|
||||||
|
|
||||||
import java.sql.SQLSyntaxErrorException;
|
import java.sql.SQLSyntaxErrorException;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -192,7 +192,7 @@ public class GlobalExceptionHandler {
|
|||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
String errorMsg = e.getMessage();
|
String errorMsg = e.getMessage();
|
||||||
if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
|
if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
|
||||||
return Result.failed(ResultCode.ACCESS_UNAUTHORIZED);
|
return Result.failed(ResultCode.DATABASE_ACCESS_DENIED);
|
||||||
} else {
|
} else {
|
||||||
return Result.failed(e.getMessage());
|
return Result.failed(e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,20 @@ public class GlobalExceptionHandler {
|
|||||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||||
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
|
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
return Result.failed(e.getMessage());
|
return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 SQL 违反了完整性约束
|
||||||
|
* <p>
|
||||||
|
* 当 SQL 违反了完整性约束时,会抛出 SQLIntegrityConstraintViolationException 异常。
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
|
||||||
|
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||||
|
public <T> Result<T> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -4,9 +4,9 @@ import cn.hutool.core.convert.Convert;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.youlai.boot.common.constant.RedisConstants;
|
import com.youlai.boot.common.constant.RedisConstants;
|
||||||
import com.youlai.boot.common.constant.SystemConstants;
|
import com.youlai.boot.common.constant.SystemConstants;
|
||||||
import com.youlai.boot.common.result.ResultCode;
|
import com.youlai.boot.core.web.ResultCode;
|
||||||
import com.youlai.boot.common.util.IPUtils;
|
import com.youlai.boot.common.util.IPUtils;
|
||||||
import com.youlai.boot.common.util.ResponseUtils;
|
import com.youlai.boot.core.web.WebResponseHelper;
|
||||||
import com.youlai.boot.system.service.ConfigService;
|
import com.youlai.boot.system.service.ConfigService;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
@@ -88,7 +88,7 @@ public class RateLimiterFilter extends OncePerRequestFilter {
|
|||||||
// 判断是否限流
|
// 判断是否限流
|
||||||
if (rateLimit(ip)) {
|
if (rateLimit(ip)) {
|
||||||
// 返回限流错误信息
|
// 返回限流错误信息
|
||||||
ResponseUtils.writeErrMsg(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
|
WebResponseHelper.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.common.result;
|
package com.youlai.boot.core.web;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.common.result;
|
package com.youlai.boot.core.web;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 响应码接口
|
* 响应码接口
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.common.result;
|
package com.youlai.boot.core.web;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.common.result;
|
package com.youlai.boot.core.web;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.common.result;
|
package com.youlai.boot.core.web;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
@@ -243,12 +243,16 @@ public enum ResultCode implements IResultCode, Serializable {
|
|||||||
|
|
||||||
TABLE_NOT_EXIST("C0311", "表不存在"),
|
TABLE_NOT_EXIST("C0311", "表不存在"),
|
||||||
COLUMN_NOT_EXIST("C0312", "列不存在"),
|
COLUMN_NOT_EXIST("C0312", "列不存在"),
|
||||||
|
DATABASE_EXECUTION_SYNTAX_ERROR("C0313", "数据库执行语法错误"),
|
||||||
|
|
||||||
MULTIPLE_SAME_NAME_COLUMNS_IN_MULTI_TABLE_ASSOCIATION("C0321", "多表关联中存在多个相同名称的列"),
|
MULTIPLE_SAME_NAME_COLUMNS_IN_MULTI_TABLE_ASSOCIATION("C0321", "多表关联中存在多个相同名称的列"),
|
||||||
|
|
||||||
DATABASE_DEADLOCK("C0331", "数据库死锁"),
|
DATABASE_DEADLOCK("C0331", "数据库死锁"),
|
||||||
|
|
||||||
PRIMARY_KEY_CONFLICT("C0341", "主键冲突"),
|
PRIMARY_KEY_CONFLICT("C0341", "主键冲突"),
|
||||||
|
INTEGRITY_CONSTRAINT_VIOLATION("C0342", "违反了完整性约束"),
|
||||||
|
|
||||||
|
DATABASE_ACCESS_DENIED("C0351", "演示环境已禁用数据库写入功能,请本地部署修改数据库链接或开启Mock模式进行体验"),
|
||||||
|
|
||||||
/** 二级宏观错误码 */
|
/** 二级宏观错误码 */
|
||||||
THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"),
|
THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"),
|
||||||
@@ -293,4 +297,4 @@ public enum ResultCode implements IResultCode, Serializable {
|
|||||||
}
|
}
|
||||||
return SYSTEM_ERROR; // 默认系统执行错误
|
return SYSTEM_ERROR; // 默认系统执行错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.youlai.boot.core.web;
|
||||||
|
|
||||||
|
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web响应辅助类
|
||||||
|
* <p>
|
||||||
|
* 用于在过滤器、处理器等无法使用 @RestControllerAdvice 的场景中统一处理响应
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class WebResponseHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入错误响应
|
||||||
|
*
|
||||||
|
* @param response HttpServletResponse
|
||||||
|
* @param resultCode 响应结果码
|
||||||
|
*/
|
||||||
|
public static void writeError(HttpServletResponse response, ResultCode resultCode) {
|
||||||
|
writeError(response, resultCode, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入错误响应(带自定义消息)
|
||||||
|
*
|
||||||
|
* @param response HttpServletResponse
|
||||||
|
* @param resultCode 响应结果码
|
||||||
|
* @param message 自定义消息
|
||||||
|
*/
|
||||||
|
public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) {
|
||||||
|
try {
|
||||||
|
// 设置HTTP状态码
|
||||||
|
int httpStatus = mapHttpStatus(resultCode);
|
||||||
|
response.setStatus(httpStatus);
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
|
||||||
|
// 构建响应对象
|
||||||
|
Result<?> result = message == null
|
||||||
|
? Result.failed(resultCode)
|
||||||
|
: Result.failed(resultCode, message);
|
||||||
|
|
||||||
|
// 写入响应
|
||||||
|
JakartaServletUtil.write(response,
|
||||||
|
JSONUtil.toJsonStr(result),
|
||||||
|
MediaType.APPLICATION_JSON_VALUE
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("写入错误响应失败: resultCode={}, message={}", resultCode, message, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据业务结果码映射HTTP状态码
|
||||||
|
*
|
||||||
|
* @param resultCode 业务结果码
|
||||||
|
* @return HTTP状态码
|
||||||
|
*/
|
||||||
|
private static int mapHttpStatus(ResultCode resultCode) {
|
||||||
|
return switch (resultCode) {
|
||||||
|
case ACCESS_UNAUTHORIZED,
|
||||||
|
ACCESS_TOKEN_INVALID,
|
||||||
|
REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
|
||||||
|
default -> HttpStatus.BAD_REQUEST.value();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.member.controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会员控制层-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class MemberController {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.member.mapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会员数据访问层-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class MemberMapper {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.member.model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会员实体-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class Member {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.member.service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会员管理服务类-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class MemberService {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.order.controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订单控制层-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class OrderController {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.order.mapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订单数据访问层-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class OrderMapper {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.order.model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订单实体-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class Order {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.order.service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订单管理服务类-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class OrderService {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.product.controller;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 商品控制层-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class ProductController {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.product.mapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 商品数据访问层-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class ProductMapper {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.product.model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 商品实体-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class Product {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.youlai.boot.modules.product.service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 会员管理服务类-业务模块演示
|
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2024/10/10
|
|
||||||
*/
|
|
||||||
public class ProductService {
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package com.youlai.boot.platform.ai.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 配置属性
|
||||||
|
*
|
||||||
|
* 优势:
|
||||||
|
* 1. 统一管理所有提供商配置
|
||||||
|
* 2. 添加新提供商只需在 yml 中添加配置,无需修改代码
|
||||||
|
* 3. 类型安全,支持 IDE 提示
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "ai")
|
||||||
|
public class AiProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用 AI 功能
|
||||||
|
*/
|
||||||
|
private Boolean enabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前使用的提供商(qwen、deepseek、openai 等)
|
||||||
|
*/
|
||||||
|
private String provider = "qwen";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有提供商的配置
|
||||||
|
* Key: 提供商名称(qwen、deepseek、openai)
|
||||||
|
* Value: 提供商配置
|
||||||
|
*/
|
||||||
|
private Map<String, ProviderConfig> providers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全配置
|
||||||
|
*/
|
||||||
|
private SecurityConfig security = new SecurityConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流配置
|
||||||
|
*/
|
||||||
|
private RateLimitConfig rateLimit = new RateLimitConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供商配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class ProviderConfig {
|
||||||
|
/**
|
||||||
|
* API Key
|
||||||
|
*/
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base URL(统一命名,符合行业惯例)
|
||||||
|
*/
|
||||||
|
private String baseUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型名称
|
||||||
|
*/
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供商显示名称(可选)
|
||||||
|
*/
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超时时间(秒)
|
||||||
|
*/
|
||||||
|
private Integer timeout = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class SecurityConfig {
|
||||||
|
private Boolean enableAudit = true;
|
||||||
|
private Boolean dangerousOperationsConfirm = true;
|
||||||
|
private java.util.List<String> functionWhitelist;
|
||||||
|
private java.util.List<String> sensitiveParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class RateLimitConfig {
|
||||||
|
private Integer maxExecutionsPerMinute = 10;
|
||||||
|
private Integer maxExecutionsPerDay = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前提供商配置
|
||||||
|
*/
|
||||||
|
public ProviderConfig getCurrentProviderConfig() {
|
||||||
|
if (providers == null || !providers.containsKey(provider)) {
|
||||||
|
throw new IllegalStateException("未找到提供商配置: " + provider);
|
||||||
|
}
|
||||||
|
return providers.get(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.youlai.boot.platform.ai.controller;
|
||||||
|
|
||||||
|
import com.youlai.boot.core.web.Result;
|
||||||
|
import com.youlai.boot.platform.ai.model.dto.*;
|
||||||
|
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 命令控制器
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Tag(name = "AI命令接口")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/ai/command")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class AiCommandController {
|
||||||
|
|
||||||
|
private final AiCommandService aiCommandService;
|
||||||
|
|
||||||
|
@Operation(summary = "解析自然语言命令")
|
||||||
|
@PostMapping("/parse")
|
||||||
|
public Result<AiCommandResponseDTO> parseCommand(
|
||||||
|
@RequestBody AiCommandRequestDTO request,
|
||||||
|
HttpServletRequest httpRequest
|
||||||
|
) {
|
||||||
|
log.info("收到AI命令解析请求: {}", request.getCommand());
|
||||||
|
|
||||||
|
try {
|
||||||
|
AiCommandResponseDTO response = aiCommandService.parseCommand(request, httpRequest);
|
||||||
|
return Result.success(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("命令解析失败", e);
|
||||||
|
return Result.success(AiCommandResponseDTO.builder()
|
||||||
|
.success(false)
|
||||||
|
.error(e.getMessage())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "执行已解析的命令")
|
||||||
|
@PostMapping("/execute")
|
||||||
|
public Result<AiExecuteResponseDTO> executeCommand(
|
||||||
|
@RequestBody AiExecuteRequestDTO request,
|
||||||
|
HttpServletRequest httpRequest
|
||||||
|
) {
|
||||||
|
log.info("收到AI命令执行请求: {}", request.getFunctionCall().getName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
AiExecuteResponseDTO response = aiCommandService.executeCommand(request, httpRequest);
|
||||||
|
return Result.success(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("命令执行失败", e);
|
||||||
|
return Result.success(AiExecuteResponseDTO.builder()
|
||||||
|
.success(false)
|
||||||
|
.error(e.getMessage())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取命令执行历史")
|
||||||
|
@GetMapping("/history")
|
||||||
|
public Result<?> getCommandHistory(
|
||||||
|
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
|
||||||
|
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size
|
||||||
|
) {
|
||||||
|
return Result.success(aiCommandService.getCommandHistory(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取可用的函数列表")
|
||||||
|
@GetMapping("/functions")
|
||||||
|
public Result<?> getAvailableFunctions() {
|
||||||
|
return Result.success(aiCommandService.getAvailableFunctions());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "撤销命令执行")
|
||||||
|
@PostMapping("/rollback/{auditId}")
|
||||||
|
public Result<?> rollbackCommand(
|
||||||
|
@Parameter(description = "审计ID") @PathVariable String auditId
|
||||||
|
) {
|
||||||
|
aiCommandService.rollbackCommand(auditId);
|
||||||
|
return Result.success("撤销成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.youlai.boot.platform.ai.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.youlai.boot.platform.ai.model.entity.AiCommandAudit;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 命令审计 Mapper
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AiCommandAuditMapper extends BaseMapper<AiCommandAudit> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.youlai.boot.platform.ai.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 命令请求 DTO
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AiCommandRequestDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户输入的自然语言命令
|
||||||
|
*/
|
||||||
|
private String command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前页面路由(用于上下文)
|
||||||
|
*/
|
||||||
|
private String currentRoute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前激活的组件名称
|
||||||
|
*/
|
||||||
|
private String currentComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 额外上下文信息
|
||||||
|
*/
|
||||||
|
private Map<String, Object> context;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.youlai.boot.platform.ai.model.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 命令解析响应 DTO
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AiCommandResponseDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否成功解析
|
||||||
|
*/
|
||||||
|
private Boolean success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析后的函数调用列表
|
||||||
|
*/
|
||||||
|
private List<FunctionCallDTO> functionCalls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 的理解和说明
|
||||||
|
*/
|
||||||
|
private String explanation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 置信度 (0-1)
|
||||||
|
*/
|
||||||
|
private Double confidence;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*/
|
||||||
|
private String error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始 LLM 响应(用于调试)
|
||||||
|
*/
|
||||||
|
private String rawResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.youlai.boot.platform.ai.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 命令执行请求 DTO
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AiExecuteRequestDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 要执行的函数调用
|
||||||
|
*/
|
||||||
|
private FunctionCallDTO functionCall;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认模式:auto=自动执行, manual=需要用户确认
|
||||||
|
*/
|
||||||
|
private String confirmMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户确认标志
|
||||||
|
*/
|
||||||
|
private Boolean userConfirmed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 幂等性令牌(防止重复执行)
|
||||||
|
*/
|
||||||
|
private String idempotencyKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.youlai.boot.platform.ai.model.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 命令执行响应 DTO
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AiExecuteResponseDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否执行成功
|
||||||
|
*/
|
||||||
|
private Boolean success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行结果数据
|
||||||
|
*/
|
||||||
|
private Object data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行结果说明
|
||||||
|
*/
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 影响的记录数
|
||||||
|
*/
|
||||||
|
private Integer affectedRows;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*/
|
||||||
|
private String error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计ID(用于追踪)
|
||||||
|
*/
|
||||||
|
private String auditId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要用户确认
|
||||||
|
*/
|
||||||
|
private Boolean requiresConfirmation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认提示信息
|
||||||
|
*/
|
||||||
|
private String confirmationPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.youlai.boot.platform.ai.model.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数调用 DTO
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class FunctionCallDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数名称
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数对象
|
||||||
|
*/
|
||||||
|
private Map<String, Object> arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.youlai.boot.platform.ai.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 命令审计记录
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("ai_command_audit")
|
||||||
|
public class AiCommandAudit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始命令
|
||||||
|
*/
|
||||||
|
private String originalCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析后的函数名称
|
||||||
|
*/
|
||||||
|
private String functionName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数参数(JSON)
|
||||||
|
*/
|
||||||
|
private String functionArguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行状态:pending, success, failed
|
||||||
|
*/
|
||||||
|
private String executeStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行结果(JSON)
|
||||||
|
*/
|
||||||
|
private String executeResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*/
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 影响的记录数
|
||||||
|
*/
|
||||||
|
private Integer affectedRows;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否危险操作
|
||||||
|
*/
|
||||||
|
private Boolean isDangerous;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要确认
|
||||||
|
*/
|
||||||
|
private Boolean requiresConfirmation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户是否确认
|
||||||
|
*/
|
||||||
|
private Boolean userConfirmed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 幂等性令牌
|
||||||
|
*/
|
||||||
|
private String idempotencyKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IP 地址
|
||||||
|
*/
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户代理
|
||||||
|
*/
|
||||||
|
private String userAgent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前路由
|
||||||
|
*/
|
||||||
|
private String currentRoute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行时间(毫秒)
|
||||||
|
*/
|
||||||
|
private Long executionTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备注
|
||||||
|
*/
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.youlai.boot.platform.ai.provider;
|
||||||
|
|
||||||
|
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.youlai.boot.platform.ai.config.AiProperties;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI 兼容协议的抽象提供商
|
||||||
|
*
|
||||||
|
* 适用于:通义千问、DeepSeek、OpenAI、ChatGLM 等兼容 OpenAI API 的模型
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public abstract class AbstractOpenAiCompatibleProvider implements AiProvider {
|
||||||
|
|
||||||
|
protected final AiProperties.ProviderConfig config;
|
||||||
|
|
||||||
|
public AbstractOpenAiCompatibleProvider(AiProperties.ProviderConfig config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String call(String systemPrompt, String userPrompt) {
|
||||||
|
if (!isConfigValid()) {
|
||||||
|
throw new IllegalStateException(getProviderName() + " 配置无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建请求体(OpenAI 标准格式)
|
||||||
|
JSONObject requestBody = JSONUtil.createObj()
|
||||||
|
.set("model", config.getModel())
|
||||||
|
.set("messages", JSONUtil.createArray()
|
||||||
|
.put(JSONUtil.createObj()
|
||||||
|
.set("role", "system")
|
||||||
|
.set("content", systemPrompt))
|
||||||
|
.put(JSONUtil.createObj()
|
||||||
|
.set("role", "user")
|
||||||
|
.set("content", userPrompt))
|
||||||
|
)
|
||||||
|
.set("temperature", 0.7);
|
||||||
|
|
||||||
|
log.info("📤 调用 {} API: {}/chat/completions", getProviderName(), config.getBaseUrl());
|
||||||
|
log.debug("请求参数: {}", requestBody);
|
||||||
|
|
||||||
|
// 发送 HTTP 请求
|
||||||
|
HttpResponse response = HttpRequest.post(config.getBaseUrl() + "/chat/completions")
|
||||||
|
.header("Authorization", "Bearer " + config.getApiKey())
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(requestBody.toString())
|
||||||
|
.timeout((int) TimeUnit.SECONDS.toMillis(config.getTimeout()))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (!response.isOk()) {
|
||||||
|
String errorMsg = String.format("%s API 调用失败: HTTP %d - %s",
|
||||||
|
getProviderName(), response.getStatus(), response.body());
|
||||||
|
log.error(errorMsg);
|
||||||
|
throw new RuntimeException(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
JSONObject responseJson = JSONUtil.parseObj(response.body());
|
||||||
|
String content = responseJson.getByPath("choices[0].message.content", String.class);
|
||||||
|
|
||||||
|
// 记录 Token 使用情况
|
||||||
|
JSONObject usage = responseJson.getJSONObject("usage");
|
||||||
|
if (usage != null) {
|
||||||
|
Integer inputTokens = usage.getInt("prompt_tokens");
|
||||||
|
Integer outputTokens = usage.getInt("completion_tokens");
|
||||||
|
Integer totalTokens = usage.getInt("total_tokens");
|
||||||
|
log.info("✅ {} 响应成功,tokens: 输入={}, 输出={}, 总计={}",
|
||||||
|
getProviderName(), inputTokens, outputTokens, totalTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("📥 {} 返回内容: {}", getProviderName(), content);
|
||||||
|
return content;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
String errorMsg = String.format("%s API 调用失败: %s", getProviderName(), e.getMessage());
|
||||||
|
log.error(errorMsg, e);
|
||||||
|
throw new RuntimeException(errorMsg, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfigValid() {
|
||||||
|
return config != null
|
||||||
|
&& StrUtil.isNotBlank(config.getApiKey())
|
||||||
|
&& StrUtil.isNotBlank(config.getBaseUrl())
|
||||||
|
&& StrUtil.isNotBlank(config.getModel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.youlai.boot.platform.ai.provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 提供商接口
|
||||||
|
*
|
||||||
|
* 策略模式:不同提供商实现各自的调用逻辑
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
*/
|
||||||
|
public interface AiProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 AI API
|
||||||
|
*
|
||||||
|
* @param systemPrompt 系统提示词
|
||||||
|
* @param userPrompt 用户提示词
|
||||||
|
* @return AI 响应内容
|
||||||
|
*/
|
||||||
|
String call(String systemPrompt, String userPrompt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提供商名称
|
||||||
|
*/
|
||||||
|
String getProviderName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查配置是否有效
|
||||||
|
*/
|
||||||
|
boolean isConfigValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.youlai.boot.platform.ai.provider;
|
||||||
|
|
||||||
|
import com.youlai.boot.platform.ai.config.AiProperties;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 提供商工厂
|
||||||
|
*
|
||||||
|
* 职责:根据配置获取对应的提供商实例
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AiProviderFactory {
|
||||||
|
|
||||||
|
private final AiProperties aiProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring 自动注入所有 AiProvider 实现类
|
||||||
|
* Key: Bean 名称(qwen、deepseek、openai)
|
||||||
|
* Value: 提供商实例
|
||||||
|
*/
|
||||||
|
private final Map<String, AiProvider> providers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前配置的提供商
|
||||||
|
*/
|
||||||
|
public AiProvider getCurrentProvider() {
|
||||||
|
String providerName = aiProperties.getProvider();
|
||||||
|
|
||||||
|
if (!providers.containsKey(providerName)) {
|
||||||
|
throw new IllegalStateException("不支持的 AI 提供商: " + providerName
|
||||||
|
+ ",可用提供商: " + providers.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
AiProvider provider = providers.get(providerName);
|
||||||
|
|
||||||
|
if (!provider.isConfigValid()) {
|
||||||
|
throw new IllegalStateException(provider.getProviderName()
|
||||||
|
+ " 配置无效,请检查 API Key、Base URL 和 Model 是否配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.youlai.boot.platform.ai.provider.impl;
|
||||||
|
|
||||||
|
import com.youlai.boot.platform.ai.config.AiProperties;
|
||||||
|
import com.youlai.boot.platform.ai.provider.AbstractOpenAiCompatibleProvider;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DeepSeek 提供商
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
*/
|
||||||
|
@Component("deepseek")
|
||||||
|
public class DeepSeekProvider extends AbstractOpenAiCompatibleProvider {
|
||||||
|
|
||||||
|
public DeepSeekProvider(AiProperties aiProperties) {
|
||||||
|
super(aiProperties.getProviders().get("deepseek"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProviderName() {
|
||||||
|
return config.getDisplayName() != null ? config.getDisplayName() : "DeepSeek";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.youlai.boot.platform.ai.provider.impl;
|
||||||
|
|
||||||
|
import com.youlai.boot.platform.ai.config.AiProperties;
|
||||||
|
import com.youlai.boot.platform.ai.provider.AbstractOpenAiCompatibleProvider;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI 提供商(GPT-4、GPT-3.5 等)
|
||||||
|
*
|
||||||
|
* 添加新提供商只需:
|
||||||
|
* 1. 继承 AbstractOpenAiCompatibleProvider
|
||||||
|
* 2. 实现 getProviderName()
|
||||||
|
* 3. 在配置文件中添加配置
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
*/
|
||||||
|
@Component("openai")
|
||||||
|
public class OpenAiProvider extends AbstractOpenAiCompatibleProvider {
|
||||||
|
|
||||||
|
public OpenAiProvider(AiProperties aiProperties) {
|
||||||
|
super(aiProperties.getProviders().get("openai"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProviderName() {
|
||||||
|
return config.getDisplayName() != null ? config.getDisplayName() : "OpenAI";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.youlai.boot.platform.ai.provider.impl;
|
||||||
|
|
||||||
|
import com.youlai.boot.platform.ai.config.AiProperties;
|
||||||
|
import com.youlai.boot.platform.ai.provider.AbstractOpenAiCompatibleProvider;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阿里通义千问提供商
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
*/
|
||||||
|
@Component("qwen")
|
||||||
|
public class QwenProvider extends AbstractOpenAiCompatibleProvider {
|
||||||
|
|
||||||
|
public QwenProvider(AiProperties aiProperties) {
|
||||||
|
super(aiProperties.getProviders().get("qwen"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProviderName() {
|
||||||
|
return config.getDisplayName() != null ? config.getDisplayName() : "阿里通义千问";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.youlai.boot.platform.ai.service;
|
||||||
|
|
||||||
|
import com.youlai.boot.platform.ai.model.dto.*;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 命令服务接口
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
public interface AiCommandService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析自然语言命令
|
||||||
|
*
|
||||||
|
* @param request 命令请求
|
||||||
|
* @param httpRequest HTTP 请求
|
||||||
|
* @return 解析结果
|
||||||
|
*/
|
||||||
|
AiCommandResponseDTO parseCommand(AiCommandRequestDTO request, HttpServletRequest httpRequest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行已解析的命令
|
||||||
|
*
|
||||||
|
* @param request 执行请求
|
||||||
|
* @param httpRequest HTTP 请求
|
||||||
|
* @return 执行结果
|
||||||
|
*/
|
||||||
|
AiExecuteResponseDTO executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取命令执行历史
|
||||||
|
*
|
||||||
|
* @param page 页码
|
||||||
|
* @param size 每页数量
|
||||||
|
* @return 历史记录
|
||||||
|
*/
|
||||||
|
Map<String, Object> getCommandHistory(Integer page, Integer size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用的函数列表
|
||||||
|
*
|
||||||
|
* @return 函数列表
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getAvailableFunctions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销命令执行
|
||||||
|
*
|
||||||
|
* @param auditId 审计ID
|
||||||
|
*/
|
||||||
|
void rollbackCommand(String auditId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
package com.youlai.boot.platform.ai.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONArray;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.youlai.boot.platform.ai.config.AiProperties;
|
||||||
|
import com.youlai.boot.platform.ai.model.dto.*;
|
||||||
|
import com.youlai.boot.platform.ai.model.entity.AiCommandAudit;
|
||||||
|
import com.youlai.boot.platform.ai.provider.AiProvider;
|
||||||
|
import com.youlai.boot.platform.ai.provider.AiProviderFactory;
|
||||||
|
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 命令服务实现类(重构版)
|
||||||
|
*
|
||||||
|
* 重构改进:
|
||||||
|
* 1. ✅ 使用策略模式 + 工厂模式管理提供商,消除 switch-case
|
||||||
|
* 2. ✅ 配置映射化,添加新提供商只需配置,无需修改代码
|
||||||
|
* 3. ✅ 统一命名为 base-url,符合行业惯例
|
||||||
|
* 4. ✅ Service 层直接返回 DTO,不包装 Result(由 Controller 统一处理)
|
||||||
|
* 5. ✅ 职责清晰,扩展性强
|
||||||
|
*
|
||||||
|
* @author Ray.Hao
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AiCommandServiceImpl implements AiCommandService {
|
||||||
|
|
||||||
|
private final AiProperties aiProperties;
|
||||||
|
private final AiProviderFactory providerFactory;
|
||||||
|
|
||||||
|
// 审计日志存储(简化实现,实际应使用数据库)
|
||||||
|
private final Map<String, AiCommandAudit> auditStore = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析自然语言命令
|
||||||
|
*
|
||||||
|
* 注意:直接返回 DTO,不包装 Result
|
||||||
|
* Controller 负责统一包装成 Result
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AiCommandResponseDTO parseCommand(AiCommandRequestDTO request, HttpServletRequest httpRequest) {
|
||||||
|
// 检查 AI 功能是否启用
|
||||||
|
if (!aiProperties.getEnabled()) {
|
||||||
|
throw new IllegalStateException("AI 功能未启用,请在配置文件中设置 ai.enabled=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前提供商(自动校验配置)
|
||||||
|
AiProvider provider = providerFactory.getCurrentProvider();
|
||||||
|
|
||||||
|
log.info("📤 使用 {} 解析命令: {}", provider.getProviderName(), request.getCommand());
|
||||||
|
|
||||||
|
// 构建提示词
|
||||||
|
String systemPrompt = buildSystemPrompt();
|
||||||
|
String userPrompt = buildUserPrompt(request);
|
||||||
|
|
||||||
|
// 调用 AI API
|
||||||
|
String response = provider.call(systemPrompt, userPrompt);
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
return parseAiResponse(response);
|
||||||
|
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// 配置错误,抛出让 Controller 处理
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析命令失败", e);
|
||||||
|
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行已解析的命令
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AiExecuteResponseDTO executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) {
|
||||||
|
// TODO: 实现命令执行逻辑
|
||||||
|
throw new UnsupportedOperationException("待实现");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取命令执行历史
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getCommandHistory(Integer page, Integer size) {
|
||||||
|
List<AiCommandAudit> allAudits = new ArrayList<>(auditStore.values());
|
||||||
|
allAudits.sort(Comparator.comparing(AiCommandAudit::getCreateTime).reversed());
|
||||||
|
|
||||||
|
int total = allAudits.size();
|
||||||
|
int start = (page - 1) * size;
|
||||||
|
int end = Math.min(start + size, total);
|
||||||
|
|
||||||
|
List<AiCommandAudit> pageData = start < total ? allAudits.subList(start, end) : new ArrayList<>();
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("list", pageData);
|
||||||
|
result.put("total", total);
|
||||||
|
result.put("page", page);
|
||||||
|
result.put("size", size);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用的函数列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getAvailableFunctions() {
|
||||||
|
List<Map<String, Object>> functions = new ArrayList<>();
|
||||||
|
|
||||||
|
// 用户管理函数
|
||||||
|
functions.add(createFunctionDef(
|
||||||
|
"deleteUser",
|
||||||
|
"删除用户",
|
||||||
|
Map.of("name", "String - 用户姓名", "id", "Long - 用户ID(可选)")
|
||||||
|
));
|
||||||
|
|
||||||
|
functions.add(createFunctionDef(
|
||||||
|
"updateUser",
|
||||||
|
"更新用户信息",
|
||||||
|
Map.of("id", "Long - 用户ID", "nickname", "String - 昵称", "status", "Integer - 状态")
|
||||||
|
));
|
||||||
|
|
||||||
|
functions.add(createFunctionDef(
|
||||||
|
"queryUsers",
|
||||||
|
"查询用户列表",
|
||||||
|
Map.of("name", "String - 姓名(可选)", "status", "Integer - 状态(可选)")
|
||||||
|
));
|
||||||
|
|
||||||
|
// 角色管理函数
|
||||||
|
functions.add(createFunctionDef(
|
||||||
|
"assignRole",
|
||||||
|
"分配角色给用户",
|
||||||
|
Map.of("userId", "Long - 用户ID", "roleIds", "List<Long> - 角色ID列表")
|
||||||
|
));
|
||||||
|
|
||||||
|
return functions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销命令执行
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void rollbackCommand(String auditId) {
|
||||||
|
AiCommandAudit audit = auditStore.get(auditId);
|
||||||
|
if (audit == null) {
|
||||||
|
throw new RuntimeException("审计记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"success".equals(audit.getExecuteStatus())) {
|
||||||
|
throw new RuntimeException("只能撤销成功执行的命令");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 实现具体的回滚逻辑
|
||||||
|
log.info("撤销命令执行: auditId={}, function={}", auditId, audit.getFunctionName());
|
||||||
|
throw new UnsupportedOperationException("回滚功能尚未实现");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建系统提示词(包含可用函数定义)
|
||||||
|
*/
|
||||||
|
private String buildSystemPrompt() {
|
||||||
|
return """
|
||||||
|
你是一个专业的命令解析助手。你的任务是将用户的自然语言命令转换为结构化的函数调用。
|
||||||
|
|
||||||
|
可用函数:
|
||||||
|
1. queryUsers - 查询用户列表
|
||||||
|
参数:keywords(搜索关键字), status(状态), deptId(部门ID)
|
||||||
|
|
||||||
|
2. deleteUser - 删除用户
|
||||||
|
参数:userId(用户ID)
|
||||||
|
|
||||||
|
3. updateUser - 更新用户信息
|
||||||
|
参数:userId(用户ID), nickname(昵称), mobile(手机号)
|
||||||
|
|
||||||
|
请将命令解析为以下 JSON 格式:
|
||||||
|
{
|
||||||
|
"functionCalls": [
|
||||||
|
{
|
||||||
|
"function": "函数名",
|
||||||
|
"parameters": { "参数名": "参数值" },
|
||||||
|
"description": "操作说明"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建用户提示词
|
||||||
|
*/
|
||||||
|
private String buildUserPrompt(AiCommandRequestDTO request) {
|
||||||
|
return "请解析以下命令:" + request.getCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 AI 响应
|
||||||
|
*/
|
||||||
|
private AiCommandResponseDTO parseAiResponse(String response) {
|
||||||
|
try {
|
||||||
|
// 提取 JSON
|
||||||
|
int jsonStart = response.indexOf("{");
|
||||||
|
int jsonEnd = response.lastIndexOf("}") + 1;
|
||||||
|
|
||||||
|
if (jsonStart == -1 || jsonEnd == 0) {
|
||||||
|
throw new IllegalArgumentException("AI 返回格式错误:未找到 JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
String jsonStr = response.substring(jsonStart, jsonEnd);
|
||||||
|
JSONObject json = JSONUtil.parseObj(jsonStr);
|
||||||
|
|
||||||
|
// 解析函数调用列表
|
||||||
|
List<FunctionCallDTO> functionCalls = new ArrayList<>();
|
||||||
|
JSONArray callsArray = json.getJSONArray("functionCalls");
|
||||||
|
|
||||||
|
if (callsArray != null) {
|
||||||
|
for (int i = 0; i < callsArray.size(); i++) {
|
||||||
|
JSONObject call = callsArray.getJSONObject(i);
|
||||||
|
functionCalls.add(FunctionCallDTO.builder()
|
||||||
|
.name(call.getStr("function"))
|
||||||
|
.arguments(call.getJSONObject("parameters") != null ?
|
||||||
|
call.getJSONObject("parameters").toBean(Map.class) : new HashMap<>())
|
||||||
|
.description(call.getStr("description"))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AiCommandResponseDTO.builder()
|
||||||
|
.success(true)
|
||||||
|
.functionCalls(functionCalls)
|
||||||
|
.rawResponse(response)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析 AI 响应失败", e);
|
||||||
|
throw new RuntimeException("解析响应失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建函数定义
|
||||||
|
*/
|
||||||
|
private Map<String, Object> createFunctionDef(String name, String description, Map<String, String> parameters) {
|
||||||
|
Map<String, Object> func = new HashMap<>();
|
||||||
|
func.put("name", name);
|
||||||
|
func.put("description", description);
|
||||||
|
func.put("parameters", parameters);
|
||||||
|
return func;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
package com.youlai.boot.shared.codegen.controller;
|
package com.youlai.boot.platform.codegen.controller;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.youlai.boot.common.result.PageResult;
|
import com.youlai.boot.core.web.PageResult;
|
||||||
import com.youlai.boot.common.result.Result;
|
import com.youlai.boot.core.web.Result;
|
||||||
import com.youlai.boot.config.property.CodegenProperties;
|
import com.youlai.boot.config.property.CodegenProperties;
|
||||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||||
import com.youlai.boot.shared.codegen.service.CodegenService;
|
import com.youlai.boot.platform.codegen.service.CodegenService;
|
||||||
import com.youlai.boot.shared.codegen.model.form.GenConfigForm;
|
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||||
import com.youlai.boot.shared.codegen.model.query.TablePageQuery;
|
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||||
import com.youlai.boot.shared.codegen.model.vo.CodegenPreviewVO;
|
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||||
import com.youlai.boot.shared.codegen.model.vo.TablePageVO;
|
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||||
import com.youlai.boot.common.annotation.Log;
|
import com.youlai.boot.common.annotation.Log;
|
||||||
import com.youlai.boot.shared.codegen.service.GenConfigService;
|
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -82,17 +82,19 @@ public class CodegenController {
|
|||||||
@Operation(summary = "获取预览生成代码")
|
@Operation(summary = "获取预览生成代码")
|
||||||
@GetMapping("/{tableName}/preview")
|
@GetMapping("/{tableName}/preview")
|
||||||
@Log(value = "预览生成代码", module = LogModuleEnum.OTHER)
|
@Log(value = "预览生成代码", module = LogModuleEnum.OTHER)
|
||||||
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName) {
|
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName,
|
||||||
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName);
|
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) {
|
||||||
|
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName, pageType);
|
||||||
return Result.success(list);
|
return Result.success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "下载代码")
|
@Operation(summary = "下载代码")
|
||||||
@GetMapping("/{tableName}/download")
|
@GetMapping("/{tableName}/download")
|
||||||
@Log(value = "下载代码", module = LogModuleEnum.OTHER)
|
@Log(value = "下载代码", module = LogModuleEnum.OTHER)
|
||||||
public void downloadZip(HttpServletResponse response, @PathVariable String tableName) {
|
public void downloadZip(HttpServletResponse response, @PathVariable String tableName,
|
||||||
|
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) {
|
||||||
String[] tableNames = tableName.split(",");
|
String[] tableNames = tableName.split(",");
|
||||||
byte[] data = codegenService.downloadCode(tableNames);
|
byte[] data = codegenService.downloadCode(tableNames, pageType);
|
||||||
|
|
||||||
response.reset();
|
response.reset();
|
||||||
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8));
|
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8));
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.youlai.boot.shared.codegen.converter;
|
package com.youlai.boot.platform.codegen.converter;
|
||||||
|
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||||
import com.youlai.boot.shared.codegen.model.form.GenConfigForm;
|
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import org.mapstruct.Mapping;
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ public interface CodegenConverter {
|
|||||||
@Mapping(source = "genConfig.packageName", target = "packageName")
|
@Mapping(source = "genConfig.packageName", target = "packageName")
|
||||||
@Mapping(source = "genConfig.entityName", target = "entityName")
|
@Mapping(source = "genConfig.entityName", target = "entityName")
|
||||||
@Mapping(source = "genConfig.author", target = "author")
|
@Mapping(source = "genConfig.author", target = "author")
|
||||||
|
@Mapping(source = "genConfig.pageType", target = "pageType")
|
||||||
|
@Mapping(source = "genConfig.removeTablePrefix", target = "removeTablePrefix")
|
||||||
@Mapping(source = "fieldConfigs", target = "fieldConfigs")
|
@Mapping(source = "fieldConfigs", target = "fieldConfigs")
|
||||||
GenConfigForm toGenConfigForm(GenConfig genConfig, List<GenFieldConfig> fieldConfigs);
|
GenConfigForm toGenConfigForm(GenConfig genConfig, List<GenFieldConfig> fieldConfigs);
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.enums;
|
package com.youlai.boot.platform.codegen.enums;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.enums;
|
package com.youlai.boot.platform.codegen.enums;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@@ -28,7 +28,8 @@ public enum JavaTypeEnum {
|
|||||||
DOUBLE("double", "Double", "number"),
|
DOUBLE("double", "Double", "number"),
|
||||||
DECIMAL("decimal", "BigDecimal", "number"),
|
DECIMAL("decimal", "BigDecimal", "number"),
|
||||||
DATE("date", "LocalDate", "Date"),
|
DATE("date", "LocalDate", "Date"),
|
||||||
DATETIME("datetime", "LocalDateTime", "Date");
|
DATETIME("datetime", "LocalDateTime", "Date"),
|
||||||
|
TIMESTAMP("timestamp", "LocalDateTime", "Date");
|
||||||
|
|
||||||
// 数据库类型
|
// 数据库类型
|
||||||
private final String dbType;
|
private final String dbType;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.enums;
|
package com.youlai.boot.platform.codegen.enums;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.youlai.boot.shared.codegen.mapper;
|
package com.youlai.boot.platform.codegen.mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.youlai.boot.shared.codegen.model.bo.ColumnMetaData;
|
import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData;
|
||||||
import com.youlai.boot.shared.codegen.model.bo.TableMetaData;
|
import com.youlai.boot.platform.codegen.model.bo.TableMetaData;
|
||||||
import com.youlai.boot.shared.codegen.model.query.TablePageQuery;
|
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||||
import com.youlai.boot.shared.codegen.model.vo.TablePageVO;
|
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.youlai.boot.shared.codegen.mapper;
|
package com.youlai.boot.platform.codegen.mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.youlai.boot.shared.codegen.mapper;
|
package com.youlai.boot.platform.codegen.mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.model.bo;
|
package com.youlai.boot.platform.codegen.model.bo;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.model.bo;
|
package com.youlai.boot.platform.codegen.model.bo;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.model.entity;
|
package com.youlai.boot.platform.codegen.model.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
|
||||||
@@ -51,4 +51,14 @@ public class GenConfig extends BaseEntity {
|
|||||||
* 作者
|
* 作者
|
||||||
*/
|
*/
|
||||||
private String author;
|
private String author;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面类型 classic|curd
|
||||||
|
*/
|
||||||
|
private String pageType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 要移除的表前缀,如: sys_
|
||||||
|
*/
|
||||||
|
private String removeTablePrefix;
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.youlai.boot.shared.codegen.model.entity;
|
package com.youlai.boot.platform.codegen.model.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.youlai.boot.common.base.BaseEntity;
|
import com.youlai.boot.common.base.BaseEntity;
|
||||||
import com.youlai.boot.shared.codegen.enums.FormTypeEnum;
|
import com.youlai.boot.platform.codegen.enums.FormTypeEnum;
|
||||||
import com.youlai.boot.shared.codegen.enums.QueryTypeEnum;
|
import com.youlai.boot.platform.codegen.enums.QueryTypeEnum;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.youlai.boot.shared.codegen.model.form;
|
package com.youlai.boot.platform.codegen.model.form;
|
||||||
|
|
||||||
import com.youlai.boot.shared.codegen.enums.FormTypeEnum;
|
import com.youlai.boot.platform.codegen.enums.FormTypeEnum;
|
||||||
import com.youlai.boot.shared.codegen.enums.QueryTypeEnum;
|
import com.youlai.boot.platform.codegen.enums.QueryTypeEnum;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -50,6 +50,12 @@ public class GenConfigForm {
|
|||||||
@Schema(description = "前端应用名")
|
@Schema(description = "前端应用名")
|
||||||
private String frontendAppName;
|
private String frontendAppName;
|
||||||
|
|
||||||
|
@Schema(description = "页面类型 classic|curd", example = "classic")
|
||||||
|
private String pageType;
|
||||||
|
|
||||||
|
@Schema(description = "要移除的表前缀,如: sys_", example = "sys_")
|
||||||
|
private String removeTablePrefix;
|
||||||
|
|
||||||
@Schema(description = "字段配置")
|
@Schema(description = "字段配置")
|
||||||
@Data
|
@Data
|
||||||
public static class FieldConfig {
|
public static class FieldConfig {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.model.query;
|
package com.youlai.boot.platform.codegen.model.query;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.youlai.boot.common.base.BasePageQuery;
|
import com.youlai.boot.common.base.BasePageQuery;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.model.vo;
|
package com.youlai.boot.platform.codegen.model.vo;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.model.vo;
|
package com.youlai.boot.platform.codegen.model.vo;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.youlai.boot.shared.codegen.service;
|
package com.youlai.boot.platform.codegen.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.youlai.boot.shared.codegen.model.query.TablePageQuery;
|
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||||
import com.youlai.boot.shared.codegen.model.vo.CodegenPreviewVO;
|
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||||
import com.youlai.boot.shared.codegen.model.vo.TablePageVO;
|
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -29,12 +29,12 @@ public interface CodegenService {
|
|||||||
* @param tableName 表名
|
* @param tableName 表名
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<CodegenPreviewVO> getCodegenPreviewData(String tableName);
|
List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载代码
|
* 下载代码
|
||||||
* @param tableNames 表名
|
* @param tableNames 表名
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
byte[] downloadCode(String[] tableNames);
|
byte[] downloadCode(String[] tableNames, String pageType);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.youlai.boot.shared.codegen.service;
|
package com.youlai.boot.platform.codegen.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||||
import com.youlai.boot.shared.codegen.model.form.GenConfigForm;
|
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 代码生成配置接口
|
* 代码生成配置接口
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.youlai.boot.shared.codegen.service;
|
package com.youlai.boot.platform.codegen.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 代码生成配置接口
|
* 代码生成配置接口
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package com.youlai.boot.shared.codegen.service.impl;
|
package com.youlai.boot.platform.codegen.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollectionUtil;
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.io.file.FileNameUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.extra.template.Template;
|
import cn.hutool.extra.template.Template;
|
||||||
@@ -10,18 +11,18 @@ import cn.hutool.extra.template.TemplateEngine;
|
|||||||
import cn.hutool.extra.template.TemplateUtil;
|
import cn.hutool.extra.template.TemplateUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.youlai.boot.shared.codegen.enums.JavaTypeEnum;
|
import com.youlai.boot.platform.codegen.enums.JavaTypeEnum;
|
||||||
import com.youlai.boot.config.property.CodegenProperties;
|
import com.youlai.boot.config.property.CodegenProperties;
|
||||||
import com.youlai.boot.shared.codegen.service.GenConfigService;
|
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
||||||
import com.youlai.boot.shared.codegen.service.GenFieldConfigService;
|
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
||||||
import com.youlai.boot.shared.codegen.service.CodegenService;
|
import com.youlai.boot.platform.codegen.service.CodegenService;
|
||||||
import com.youlai.boot.common.exception.BusinessException;
|
import com.youlai.boot.core.exception.BusinessException;
|
||||||
import com.youlai.boot.shared.codegen.mapper.DatabaseMapper;
|
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||||
import com.youlai.boot.shared.codegen.model.query.TablePageQuery;
|
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||||
import com.youlai.boot.shared.codegen.model.vo.CodegenPreviewVO;
|
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||||
import com.youlai.boot.shared.codegen.model.vo.TablePageVO;
|
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -72,7 +73,7 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
* @return 预览数据
|
* @return 预览数据
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName) {
|
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType) {
|
||||||
|
|
||||||
List<CodegenPreviewVO> list = new ArrayList<>();
|
List<CodegenPreviewVO> list = new ArrayList<>();
|
||||||
|
|
||||||
@@ -124,7 +125,9 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
|
|
||||||
/* 3. 生成文件内容 */
|
/* 3. 生成文件内容 */
|
||||||
// 将模板文件中的变量替换为具体的值 生成代码内容
|
// 将模板文件中的变量替换为具体的值 生成代码内容
|
||||||
String content = getCodeContent(templateConfig, genConfig, fieldConfigs);
|
// 优先使用保存的 ui,没有则使用请求参数
|
||||||
|
String finalType = StrUtil.blankToDefault(genConfig.getPageType(), pageType);
|
||||||
|
String content = getCodeContent(templateConfig, genConfig, fieldConfigs, finalType);
|
||||||
previewVO.setContent(content);
|
previewVO.setContent(content);
|
||||||
|
|
||||||
list.add(previewVO);
|
list.add(previewVO);
|
||||||
@@ -146,7 +149,8 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
} else if ("MapperXml".equals(templateName)) {
|
} else if ("MapperXml".equals(templateName)) {
|
||||||
return entityName + "Mapper" + extension;
|
return entityName + "Mapper" + extension;
|
||||||
} else if ("API".equals(templateName)) {
|
} else if ("API".equals(templateName)) {
|
||||||
return StrUtil.toSymbolCase(entityName, '-') + extension;
|
// 生成 user-api.ts 命名
|
||||||
|
return StrUtil.toSymbolCase(entityName, '-') + "-api" + extension;
|
||||||
} else if ("VIEW".equals(templateName)) {
|
} else if ("VIEW".equals(templateName)) {
|
||||||
return "index.vue";
|
return "index.vue";
|
||||||
}
|
}
|
||||||
@@ -211,7 +215,7 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
* @param fieldConfigs 字段配置
|
* @param fieldConfigs 字段配置
|
||||||
* @return 代码内容
|
* @return 代码内容
|
||||||
*/
|
*/
|
||||||
private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List<GenFieldConfig> fieldConfigs) {
|
private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List<GenFieldConfig> fieldConfigs, String pageType) {
|
||||||
|
|
||||||
Map<String, Object> bindMap = new HashMap<>();
|
Map<String, Object> bindMap = new HashMap<>();
|
||||||
|
|
||||||
@@ -252,7 +256,12 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
bindMap.put("hasRequiredField", hasRequiredField);
|
bindMap.put("hasRequiredField", hasRequiredField);
|
||||||
|
|
||||||
TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
|
TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
|
||||||
Template template = templateEngine.getTemplate(templateConfig.getTemplatePath());
|
// 根据 ui 选择不同的前端页面模板:默认 index.vue.vm;封装版使用 index.curd.vue.vm
|
||||||
|
String path = templateConfig.getTemplatePath();
|
||||||
|
if ("curd".equalsIgnoreCase(pageType) && path.endsWith("index.vue.vm")) {
|
||||||
|
path = path.replace("index.vue.vm", "index.curd.vue.vm");
|
||||||
|
}
|
||||||
|
Template template = templateEngine.getTemplate(path);
|
||||||
|
|
||||||
return template.render(bindMap);
|
return template.render(bindMap);
|
||||||
}
|
}
|
||||||
@@ -264,13 +273,13 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
* @return 压缩文件字节数组
|
* @return 压缩文件字节数组
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public byte[] downloadCode(String[] tableNames) {
|
public byte[] downloadCode(String[] tableNames, String ui) {
|
||||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
ZipOutputStream zip = new ZipOutputStream(outputStream)) {
|
ZipOutputStream zip = new ZipOutputStream(outputStream)) {
|
||||||
|
|
||||||
// 遍历每个表名,生成对应的代码并压缩到 zip 文件中
|
// 遍历每个表名,生成对应的代码并压缩到 zip 文件中
|
||||||
for (String tableName : tableNames) {
|
for (String tableName : tableNames) {
|
||||||
generateAndZipCode(tableName, zip);
|
generateAndZipCode(tableName, zip, ui);
|
||||||
}
|
}
|
||||||
// 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整
|
// 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整
|
||||||
zip.finish();
|
zip.finish();
|
||||||
@@ -288,8 +297,8 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
* @param tableName 表名
|
* @param tableName 表名
|
||||||
* @param zip 压缩文件输出流
|
* @param zip 压缩文件输出流
|
||||||
*/
|
*/
|
||||||
private void generateAndZipCode(String tableName, ZipOutputStream zip) {
|
private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui) {
|
||||||
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName);
|
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName, ui);
|
||||||
|
|
||||||
for (CodegenPreviewVO codePreview : codePreviewList) {
|
for (CodegenPreviewVO codePreview : codePreviewList) {
|
||||||
String fileName = codePreview.getFileName();
|
String fileName = codePreview.getFileName();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.codegen.service.impl;
|
package com.youlai.boot.platform.codegen.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollectionUtil;
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import cn.hutool.core.lang.Assert;
|
import cn.hutool.core.lang.Assert;
|
||||||
@@ -7,21 +7,21 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.youlai.boot.YouLaiBootApplication;
|
import com.youlai.boot.YouLaiBootApplication;
|
||||||
import com.youlai.boot.common.enums.EnvEnum;
|
import com.youlai.boot.common.enums.EnvEnum;
|
||||||
import com.youlai.boot.shared.codegen.enums.FormTypeEnum;
|
import com.youlai.boot.platform.codegen.enums.FormTypeEnum;
|
||||||
import com.youlai.boot.shared.codegen.enums.JavaTypeEnum;
|
import com.youlai.boot.platform.codegen.enums.JavaTypeEnum;
|
||||||
import com.youlai.boot.shared.codegen.enums.QueryTypeEnum;
|
import com.youlai.boot.platform.codegen.enums.QueryTypeEnum;
|
||||||
import com.youlai.boot.common.exception.BusinessException;
|
import com.youlai.boot.core.exception.BusinessException;
|
||||||
import com.youlai.boot.config.property.CodegenProperties;
|
import com.youlai.boot.config.property.CodegenProperties;
|
||||||
import com.youlai.boot.shared.codegen.converter.CodegenConverter;
|
import com.youlai.boot.platform.codegen.converter.CodegenConverter;
|
||||||
import com.youlai.boot.shared.codegen.mapper.DatabaseMapper;
|
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
||||||
import com.youlai.boot.shared.codegen.mapper.GenConfigMapper;
|
import com.youlai.boot.platform.codegen.mapper.GenConfigMapper;
|
||||||
import com.youlai.boot.shared.codegen.model.bo.ColumnMetaData;
|
import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData;
|
||||||
import com.youlai.boot.shared.codegen.model.bo.TableMetaData;
|
import com.youlai.boot.platform.codegen.model.bo.TableMetaData;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||||
import com.youlai.boot.shared.codegen.model.form.GenConfigForm;
|
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||||
import com.youlai.boot.shared.codegen.service.GenConfigService;
|
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
||||||
import com.youlai.boot.shared.codegen.service.GenFieldConfigService;
|
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
||||||
import com.youlai.boot.system.service.MenuService;
|
import com.youlai.boot.system.service.MenuService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -83,8 +83,13 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
|||||||
if (StrUtil.isNotBlank(tableComment)) {
|
if (StrUtil.isNotBlank(tableComment)) {
|
||||||
genConfig.setBusinessName(tableComment.replace("表", "").trim());
|
genConfig.setBusinessName(tableComment.replace("表", "").trim());
|
||||||
}
|
}
|
||||||
// 根据表名生成实体类名 例如:sys_user -> SysUser
|
// 根据表名生成实体类名,支持去除前缀 例如:sys_user -> SysUser
|
||||||
genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName))));
|
String removePrefix = genConfig.getRemoveTablePrefix();
|
||||||
|
String processedTable = tableName;
|
||||||
|
if (StrUtil.isNotBlank(removePrefix) && StrUtil.startWith(tableName, removePrefix)) {
|
||||||
|
processedTable = StrUtil.removePrefix(tableName, removePrefix);
|
||||||
|
}
|
||||||
|
genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable))));
|
||||||
|
|
||||||
genConfig.setPackageName(YouLaiBootApplication.class.getPackageName());
|
genConfig.setPackageName(YouLaiBootApplication.class.getPackageName());
|
||||||
genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名
|
genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.youlai.boot.shared.codegen.service.impl;
|
package com.youlai.boot.platform.codegen.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.youlai.boot.shared.codegen.mapper.GenFieldConfigMapper;
|
import com.youlai.boot.platform.codegen.mapper.GenFieldConfigMapper;
|
||||||
import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||||
import com.youlai.boot.shared.codegen.service.GenFieldConfigService;
|
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.youlai.boot.shared.file.controller;
|
package com.youlai.boot.platform.file.controller;
|
||||||
|
|
||||||
import com.youlai.boot.common.result.Result;
|
import com.youlai.boot.core.web.Result;
|
||||||
import com.youlai.boot.shared.file.service.FileService;
|
import com.youlai.boot.platform.file.service.FileService;
|
||||||
import com.youlai.boot.shared.file.model.FileInfo;
|
import com.youlai.boot.platform.file.model.FileInfo;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.file.model;
|
package com.youlai.boot.platform.file.model;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.youlai.boot.shared.file.service;
|
package com.youlai.boot.platform.file.service;
|
||||||
|
|
||||||
import com.youlai.boot.shared.file.model.FileInfo;
|
import com.youlai.boot.platform.file.model.FileInfo;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.file.service.impl;
|
package com.youlai.boot.platform.file.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
@@ -8,8 +8,8 @@ import com.aliyun.oss.OSS;
|
|||||||
import com.aliyun.oss.OSSClientBuilder;
|
import com.aliyun.oss.OSSClientBuilder;
|
||||||
import com.aliyun.oss.model.ObjectMetadata;
|
import com.aliyun.oss.model.ObjectMetadata;
|
||||||
import com.aliyun.oss.model.PutObjectRequest;
|
import com.aliyun.oss.model.PutObjectRequest;
|
||||||
import com.youlai.boot.shared.file.service.FileService;
|
import com.youlai.boot.platform.file.service.FileService;
|
||||||
import com.youlai.boot.shared.file.model.FileInfo;
|
import com.youlai.boot.platform.file.model.FileInfo;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.youlai.boot.shared.file.service.impl;
|
package com.youlai.boot.platform.file.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.date.DatePattern;
|
import cn.hutool.core.date.DatePattern;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import com.youlai.boot.shared.file.model.FileInfo;
|
import com.youlai.boot.platform.file.model.FileInfo;
|
||||||
import com.youlai.boot.shared.file.service.FileService;
|
import com.youlai.boot.platform.file.service.FileService;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.youlai.boot.shared.file.service.impl;
|
package com.youlai.boot.platform.file.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.lang.Assert;
|
import cn.hutool.core.lang.Assert;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.youlai.boot.common.exception.BusinessException;
|
import com.youlai.boot.core.exception.BusinessException;
|
||||||
import com.youlai.boot.common.result.ResultCode;
|
import com.youlai.boot.core.web.ResultCode;
|
||||||
import com.youlai.boot.shared.file.model.FileInfo;
|
import com.youlai.boot.platform.file.model.FileInfo;
|
||||||
import com.youlai.boot.shared.file.service.FileService;
|
import com.youlai.boot.platform.file.service.FileService;
|
||||||
import io.minio.*;
|
import io.minio.*;
|
||||||
import io.minio.http.Method;
|
import io.minio.http.Method;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.youlai.boot.shared.mail.controller;
|
package com.youlai.boot.platform.mail.controller;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 邮件控制层
|
* 邮件控制层
|
||||||
*
|
*
|
||||||
* @author Ray
|
* @author Ray.Hao
|
||||||
* @since 2.10.0
|
* @since 2.10.0
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.mail.service;
|
package com.youlai.boot.platform.mail.service;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 邮件服务接口层
|
* 邮件服务接口层
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.youlai.boot.shared.mail.service.impl;
|
package com.youlai.boot.platform.mail.service.impl;
|
||||||
|
|
||||||
import com.youlai.boot.config.property.MailProperties;
|
import com.youlai.boot.config.property.MailProperties;
|
||||||
import com.youlai.boot.shared.mail.service.MailService;
|
import com.youlai.boot.platform.mail.service.MailService;
|
||||||
import jakarta.mail.MessagingException;
|
import jakarta.mail.MessagingException;
|
||||||
import jakarta.mail.internet.MimeMessage;
|
import jakarta.mail.internet.MimeMessage;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.sms.controller;
|
package com.youlai.boot.platform.sms.controller;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 短信控制层
|
* 短信控制层
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.sms.enums;
|
package com.youlai.boot.platform.sms.enums;
|
||||||
|
|
||||||
import com.youlai.boot.common.base.IBaseEnum;
|
import com.youlai.boot.common.base.IBaseEnum;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.youlai.boot.shared.sms.service;
|
package com.youlai.boot.platform.sms.service;
|
||||||
|
|
||||||
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
|
import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.sms.service.impl;
|
package com.youlai.boot.platform.sms.service.impl;
|
||||||
|
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.aliyuncs.CommonRequest;
|
import com.aliyuncs.CommonRequest;
|
||||||
@@ -9,8 +9,8 @@ import com.aliyuncs.exceptions.ClientException;
|
|||||||
import com.aliyuncs.http.MethodType;
|
import com.aliyuncs.http.MethodType;
|
||||||
import com.aliyuncs.profile.DefaultProfile;
|
import com.aliyuncs.profile.DefaultProfile;
|
||||||
import com.youlai.boot.config.property.AliyunSmsProperties;
|
import com.youlai.boot.config.property.AliyunSmsProperties;
|
||||||
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
|
import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||||
import com.youlai.boot.shared.sms.service.SmsService;
|
import com.youlai.boot.platform.sms.service.SmsService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.youlai.boot.shared.websocket.controller;
|
package com.youlai.boot.platform.websocket.controller;
|
||||||
|
|
||||||
import com.youlai.boot.shared.websocket.model.ChatMessage;
|
import com.youlai.boot.platform.websocket.model.ChatMessage;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.messaging.handler.annotation.DestinationVariable;
|
import org.springframework.messaging.handler.annotation.DestinationVariable;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.shared.websocket.model;
|
package com.youlai.boot.platform.websocket.model;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.youlai.boot.plugin.knife4j;
|
||||||
|
|
||||||
|
|
||||||
|
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
|
||||||
|
import com.github.xiaoymin.knife4j.core.conf.ExtensionsConstants;
|
||||||
|
import com.github.xiaoymin.knife4j.core.conf.GlobalConstants;
|
||||||
|
import com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties;
|
||||||
|
import com.github.xiaoymin.knife4j.spring.configuration.Knife4jSetting;
|
||||||
|
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||||
|
import org.springdoc.core.properties.SpringDocConfigProperties;
|
||||||
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
|
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增强扩展属性支持
|
||||||
|
* @since 4.1.0
|
||||||
|
* @author <a href="xiaoymin@foxmail.com">xiaoymin@foxmail.com</a>
|
||||||
|
* 2022/12/11 22:40
|
||||||
|
*/
|
||||||
|
@Primary
|
||||||
|
@Configuration
|
||||||
|
@Slf4j
|
||||||
|
public class Knife4jOpenApiCustomizer extends com.github.xiaoymin.knife4j.spring.extension.Knife4jOpenApiCustomizer implements GlobalOpenApiCustomizer {
|
||||||
|
final Knife4jProperties knife4jProperties;
|
||||||
|
final SpringDocConfigProperties properties;
|
||||||
|
public Knife4jOpenApiCustomizer(Knife4jProperties knife4jProperties, SpringDocConfigProperties properties) {
|
||||||
|
super(knife4jProperties,properties);
|
||||||
|
this.knife4jProperties = knife4jProperties;
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customise(OpenAPI openApi) {
|
||||||
|
log.debug("Knife4j OpenApiCustomizer");
|
||||||
|
if (knife4jProperties.isEnable()) {
|
||||||
|
Knife4jSetting setting = knife4jProperties.getSetting();
|
||||||
|
OpenApiExtensionResolver openApiExtensionResolver = new OpenApiExtensionResolver(setting, knife4jProperties.getDocuments());
|
||||||
|
// 解析初始化
|
||||||
|
openApiExtensionResolver.start();
|
||||||
|
Map<String, Object> objectMap = new HashMap<>();
|
||||||
|
objectMap.put(GlobalConstants.EXTENSION_OPEN_SETTING_NAME, setting);
|
||||||
|
objectMap.put(GlobalConstants.EXTENSION_OPEN_MARKDOWN_NAME, openApiExtensionResolver.getMarkdownFiles());
|
||||||
|
openApi.addExtension(GlobalConstants.EXTENSION_OPEN_API_NAME, objectMap);
|
||||||
|
addOrderExtension(openApi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 往OpenAPI内tags字段添加x-order属性
|
||||||
|
*
|
||||||
|
* @param openApi openApi
|
||||||
|
*/
|
||||||
|
|
||||||
|
private void addOrderExtension(OpenAPI openApi) {
|
||||||
|
if (CollectionUtils.isEmpty(properties.getGroupConfigs())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取包扫描路径
|
||||||
|
Set<String> packagesToScan =
|
||||||
|
properties.getGroupConfigs().stream()
|
||||||
|
.map(SpringDocConfigProperties.GroupConfig::getPackagesToScan)
|
||||||
|
.filter(toScan -> !CollectionUtils.isEmpty(toScan))
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (CollectionUtils.isEmpty(packagesToScan)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 扫描包下被ApiSupport注解的RestController Class
|
||||||
|
Set<Class<?>> classes =
|
||||||
|
packagesToScan.stream()
|
||||||
|
.map(packageToScan -> scanPackageByAnnotation(packageToScan, RestController.class))
|
||||||
|
.flatMap(Set::stream)
|
||||||
|
.filter(clazz -> clazz.isAnnotationPresent(ApiSupport.class))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (!CollectionUtils.isEmpty(classes)) {
|
||||||
|
// ApiSupport oder值存入tagSortMap<Tag.name,ApiSupport.order>
|
||||||
|
Map<String, Integer> tagOrderMap = new HashMap<>();
|
||||||
|
classes.forEach(
|
||||||
|
clazz -> {
|
||||||
|
Tag tag = getTag(clazz);
|
||||||
|
if (Objects.nonNull(tag)) {
|
||||||
|
ApiSupport apiSupport = clazz.getAnnotation(ApiSupport.class);
|
||||||
|
tagOrderMap.putIfAbsent(tag.name(), apiSupport.order());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 往openApi tags字段添加x-order增强属性
|
||||||
|
if (openApi.getTags() != null) {
|
||||||
|
openApi
|
||||||
|
.getTags()
|
||||||
|
.forEach(
|
||||||
|
tag -> {
|
||||||
|
if (tagOrderMap.containsKey(tag.getName())) {
|
||||||
|
tag.addExtension(
|
||||||
|
ExtensionsConstants.EXTENSION_ORDER, tagOrderMap.get(tag.getName()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tag getTag(Class<?> clazz) {
|
||||||
|
// 从类上获取
|
||||||
|
Tag tag = clazz.getAnnotation(Tag.class);
|
||||||
|
if (Objects.isNull(tag)) {
|
||||||
|
// 从接口上获取
|
||||||
|
Class<?>[] interfaces = clazz.getInterfaces();
|
||||||
|
if (ArrayUtils.isNotEmpty(interfaces)) {
|
||||||
|
for (Class<?> interfaceClazz : interfaces) {
|
||||||
|
Tag anno = interfaceClazz.getAnnotation(Tag.class);
|
||||||
|
if (Objects.nonNull(anno)) {
|
||||||
|
tag = anno;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Class<?>> scanPackageByAnnotation(
|
||||||
|
String packageName, final Class<? extends Annotation> annotationClass) {
|
||||||
|
ClassPathScanningCandidateComponentProvider scanner =
|
||||||
|
new ClassPathScanningCandidateComponentProvider(false);
|
||||||
|
scanner.addIncludeFilter(new AnnotationTypeFilter(annotationClass));
|
||||||
|
Set<Class<?>> classes = new HashSet<>();
|
||||||
|
for (BeanDefinition beanDefinition : scanner.findCandidateComponents(packageName)) {
|
||||||
|
try {
|
||||||
|
Class<?> clazz = Class.forName(beanDefinition.getBeanClassName());
|
||||||
|
classes.add(clazz);
|
||||||
|
} catch (ClassNotFoundException ignore) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.core.handler;
|
package com.youlai.boot.plugin.mybatis;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||||
@@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
|
|||||||
import com.youlai.boot.common.annotation.DataPermission;
|
import com.youlai.boot.common.annotation.DataPermission;
|
||||||
import com.youlai.boot.common.base.IBaseEnum;
|
import com.youlai.boot.common.base.IBaseEnum;
|
||||||
import com.youlai.boot.common.enums.DataScopeEnum;
|
import com.youlai.boot.common.enums.DataScopeEnum;
|
||||||
import com.youlai.boot.core.security.util.SecurityUtils;
|
import com.youlai.boot.security.util.SecurityUtils;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.sf.jsqlparser.expression.Expression;
|
import net.sf.jsqlparser.expression.Expression;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.core.handler;
|
package com.youlai.boot.plugin.mybatis;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||||
import org.apache.ibatis.reflection.MetaObject;
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.youlai.boot.core.security.exception;
|
package com.youlai.boot.security.exception;
|
||||||
|
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user