wip:临时提交
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,4 +15,5 @@ logs
|
||||
|
||||
docker/*/data/
|
||||
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>
|
||||
|
||||
# 设置国内镜像源(中国科技大学镜像源),修改容器时区(alpine镜像需安装tzdata来设置时区),安装字体库(验证码)
|
||||
RUN echo -e https://mirrors.ustc.edu.cn/alpine/v3.7/main/ > /etc/apk/repositories \
|
||||
&& 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
|
||||
# 设置时区(Debian直接使用环境变量)
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 在运行时自动挂载 /tmp 目录为匿名卷,提高可移植性。如果 /tmp 目录没有挂载为卷,这些文件会写入容器的可写层,可能导致容器镜像膨胀。
|
||||
# 在运行时自动挂载 /tmp 目录为匿名卷
|
||||
VOLUME /tmp
|
||||
|
||||
# 将构建的 Spring Boot 可执行 JAR 复制到容器中,重命名为 app.jar
|
||||
# 添加应用
|
||||
ADD target/youlai-boot.jar app.jar
|
||||
|
||||
# 指定容器启动时执行的命令
|
||||
# 启动命令
|
||||
CMD java \
|
||||
-Xms512m -Xmx512m \
|
||||
-Djava.security.egd=file:/dev/./urandom \
|
||||
-jar /app.jar
|
||||
|
||||
# 暴露容器的端口
|
||||
# 暴露端口
|
||||
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">
|
||||
<img alt="logo" width="100" height="100" src="https://foruda.gitee.com/images/1733417239320800627/3c5290fe_716974.png">
|
||||
<h2>youlai-boot</h2>
|
||||
<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">
|
||||
<img alt="有来技术" src="https://gitee.com/youlaiorg/youlai-boot/badge/star.svg"/>
|
||||
</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) |
|
||||
| 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) |
|
||||
@@ -70,47 +95,55 @@ youlai-boot
|
||||
├── sql # SQL脚本
|
||||
│ ├── mysql # MySQL 脚本
|
||||
├── src # 源码目录
|
||||
│ ├── auth # 认证模块(登录入口)
|
||||
│ ├── common # 公共模块
|
||||
│ │ ├── annotation # 注解定义
|
||||
│ │ ├── base # 基础类
|
||||
│ │ ├── constant # 常量
|
||||
│ │ ├── enums # 枚举类型
|
||||
│ │ ├── exception # 异常处理
|
||||
│ │ ├── model # 数据模型
|
||||
│ │ ├── result # 结果封装
|
||||
│ │ └── util # 工具类
|
||||
│ ├── config # 自动装配配置
|
||||
│ │ └── property # 配置属性目录
|
||||
│ ├── core # 核心功能
|
||||
│ ├── core # 核心框架
|
||||
│ │ ├── aspect # 切面(日志、防重提交)
|
||||
│ │ ├── exception # 异常处理
|
||||
│ │ ├── filter # 过滤器(请求日志、限流)
|
||||
│ │ ├── handler # 处理器(数据权限、数据填充)
|
||||
│ │ └── security # Spring Security 安全模块
|
||||
│ ├── modules # 业务模块
|
||||
│ │ ├── member # 会员模块【业务模块演示】
|
||||
│ │ ├── order # 订单模块【业务模块演示】
|
||||
│ │ ├── product # 商品模块【业务模块演示】
|
||||
│ ├── shared # 共享模块
|
||||
│ │ ├── auth # 认证模块
|
||||
│ │ ├── file # 文件模块
|
||||
│ │ ├── validator # 验证器
|
||||
│ │ └── web # Web响应封装(Result、PageResult等)
|
||||
│ ├── platform # 平台服务(通用服务)
|
||||
│ │ ├── codegen # 代码生成模块
|
||||
│ │ ├── mail # 邮件模块
|
||||
│ │ ├── sms # 短信模块
|
||||
│ │ └── websocket # WebSocket 模块
|
||||
│ │ ├── file # 文件服务
|
||||
│ │ ├── mail # 邮件服务
|
||||
│ │ ├── sms # 短信服务
|
||||
│ │ └── websocket # WebSocket服务
|
||||
│ ├── plugin # 插件扩展
|
||||
│ │ ├── knife4j # Knife4j 扩展
|
||||
│ │ └── mybatis # Mybatis 扩展
|
||||
│ ├── security # 安全框架(Spring Security)
|
||||
│ │ ├── exception # 安全异常
|
||||
│ │ ├── filter # 安全过滤器
|
||||
│ │ ├── handler # 安全处理器
|
||||
│ │ ├── model # 安全模型
|
||||
│ │ ├── provider # 认证提供者
|
||||
│ │ ├── service # 安全服务
|
||||
│ │ ├── token # Token管理
|
||||
│ │ └── util # 安全工具类
|
||||
│ ├── system # 系统模块
|
||||
│ │ ├── controller # 控制层
|
||||
│ │ ├── converter # MapStruct 转换器
|
||||
│ │ ├── event # 事件处理
|
||||
│ │ ├── enums # 枚举
|
||||
│ │ ├── handler # 处理器
|
||||
│ │ ├── listener # 监听器
|
||||
│ │ ├── mapper # 数据库访问层
|
||||
│ │ ├── model # 模型层
|
||||
│ │ │ ├── bo # 业务对象
|
||||
│ │ │ ├── dto # 数据传输对象
|
||||
│ │ │ ├── entity # 实体对象
|
||||
│ │ │ ├── event # 事件对象
|
||||
│ │ │ ├── form # 表单对象
|
||||
│ │ │ ├── query # 查询参数对象
|
||||
│ │ │ └── vo # 视图对象
|
||||
│ │ ├── mapper # 数据库访问层
|
||||
│ │ └── service # 业务逻辑层
|
||||
│ └── YouLaiBootApplication # 启动类
|
||||
└── end
|
||||
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
- youlai-boot
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
image: minio/minio:RELEASE.2024-07-16T23-46-41Z
|
||||
container_name: minio
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
@@ -66,4 +66,4 @@ services:
|
||||
ports:
|
||||
- 8080:8080
|
||||
networks:
|
||||
- youlai-boot
|
||||
- youlai-boot
|
||||
|
||||
@@ -133,7 +133,7 @@ protected-mode no
|
||||
#
|
||||
# enable-protected-configs no
|
||||
# enable-debug-command no
|
||||
# enable-module-command no
|
||||
# enable-business-command no
|
||||
|
||||
# Accept connections on the specified port, default is 6379 (IANA #815344).
|
||||
# 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
|
||||
# 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.
|
||||
# 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
|
||||
# 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.
|
||||
@@ -964,7 +964,7 @@ replica-priority 100
|
||||
#
|
||||
# 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
|
||||
# alice can use, and later DEBUG was removed. However if we invert the order
|
||||
# of two ACL rules the result will be different:
|
||||
@@ -1066,7 +1066,7 @@ acllog-max-len 128
|
||||
# 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
|
||||
# hard to guess so that it will still be available for internal-use tools
|
||||
# but not available for general clients.
|
||||
@@ -1095,7 +1095,7 @@ acllog-max-len 128
|
||||
# an error 'max number of clients reached'.
|
||||
#
|
||||
# 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
|
||||
# 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.
|
||||
# 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
|
||||
# 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>
|
||||
<artifactId>youlai-boot</artifactId>
|
||||
<version>2.22.0</version>
|
||||
<version>3.3.0</version>
|
||||
<description>基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<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/>
|
||||
</parent>
|
||||
|
||||
@@ -25,15 +25,16 @@
|
||||
<mysql-connector-j.version>9.1.0</mysql-connector-j.version>
|
||||
<druid.version>1.2.24</druid.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>
|
||||
|
||||
<mapstruct.version>1.6.3</mapstruct.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>
|
||||
@@ -42,7 +43,7 @@
|
||||
<aliyun-sdk-oss.version>3.16.3</aliyun-sdk-oss.version>
|
||||
|
||||
<!-- 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>
|
||||
@@ -52,11 +53,11 @@
|
||||
<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>
|
||||
|
||||
<!-- 微信 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>
|
||||
</properties>
|
||||
|
||||
@@ -152,6 +153,17 @@
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<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>
|
||||
|
||||
<!-- MapStruct 对象映射 -->
|
||||
@@ -248,6 +260,13 @@
|
||||
<version>${caffeine.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 动态多数据源 -->
|
||||
<!--<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||
<version>${dynamic-datasource.version}</version>
|
||||
</dependency>-->
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -260,4 +279,4 @@
|
||||
</plugins>
|
||||
</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 (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 (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 (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 (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', '菜单二级', 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 (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);
|
||||
@@ -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 (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 (37, 36, '0,36', '富文本编辑器', 1, NULL, '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 (39, 36, '0,36', '图标选择器', 1, NULL, 'icon-selector', 'demo/icon-selector', NULL, NULL, 1, 1, 4, '', '', now(), now(), 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, 'Upload', 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', 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 (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 (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);
|
||||
@@ -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 (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 (90, 89, '0,89', 'Websocket', 1, NULL, '/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, NULL, 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', 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, '', 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', 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 (95, 36, '0,36', '字典组件', 1, 'DictDemo', '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 (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 (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 (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 (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 (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, '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 (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 (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 (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, '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 (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);
|
||||
@@ -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 (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 (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 (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 (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 (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 (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', '', now(), now(), 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, '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
|
||||
@@ -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, 146);
|
||||
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
|
||||
@@ -374,7 +382,7 @@ CREATE TABLE `sys_user` (
|
||||
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
|
||||
`openid` char(28) COMMENT '微信 openid',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `login_name`(`username` ASC) USING BTREE
|
||||
KEY `login_name` (`username`)
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户信息表';
|
||||
|
||||
-- ----------------------------
|
||||
@@ -442,9 +450,11 @@ CREATE TABLE `gen_config` (
|
||||
`entity_name` varchar(100) NOT NULL COMMENT '实体类名',
|
||||
`author` varchar(50) NOT NULL COMMENT '作者',
|
||||
`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 '创建时间',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`is_deleted` bit(1) DEFAULT b'0' COMMENT '是否删除',
|
||||
`is_deleted` tinyint(4) DEFAULT 0 COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tablename` (`table_name`)
|
||||
) 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 (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.result.Result;
|
||||
import com.youlai.boot.shared.auth.service.AuthService;
|
||||
import com.youlai.boot.shared.auth.model.CaptchaInfo;
|
||||
import com.youlai.boot.core.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||
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.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* 认证控制层
|
||||
*
|
||||
@@ -28,10 +32,10 @@ public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@Operation(summary = "获取登录验证码")
|
||||
@Operation(summary = "获取验证码")
|
||||
@GetMapping("/captcha")
|
||||
public Result<CaptchaInfo> getCaptcha() {
|
||||
CaptchaInfo captcha = authService.getCaptcha();
|
||||
public Result<CaptchaVO> getCaptcha() {
|
||||
CaptchaVO captcha = authService.getCaptcha();
|
||||
return Result.success(captcha);
|
||||
}
|
||||
|
||||
@@ -46,42 +50,6 @@ public class AuthController {
|
||||
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 = "短信验证码登录")
|
||||
@PostMapping("/login/sms")
|
||||
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
|
||||
@@ -92,4 +60,56 @@ public class AuthController {
|
||||
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
|
||||
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 lombok.Builder;
|
||||
@@ -13,7 +13,7 @@ import lombok.Data;
|
||||
@Schema(description = "验证码信息")
|
||||
@Data
|
||||
@Builder
|
||||
public class CaptchaInfo {
|
||||
public class CaptchaVO {
|
||||
|
||||
@Schema(description = "验证码缓存 Key")
|
||||
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.core.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
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 验证码
|
||||
*/
|
||||
CaptchaInfo getCaptcha();
|
||||
CaptchaVO getCaptcha();
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
@@ -48,6 +50,22 @@ public interface AuthService {
|
||||
*/
|
||||
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.CaptchaUtil;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
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.SecurityConstants;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.common.enums.CaptchaTypeEnum;
|
||||
import com.youlai.boot.config.property.CaptchaProperties;
|
||||
import com.youlai.boot.core.security.extension.sms.SmsAuthenticationToken;
|
||||
import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationToken;
|
||||
import com.youlai.boot.core.security.util.SecurityUtils;
|
||||
import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum;
|
||||
import com.youlai.boot.core.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.shared.auth.model.CaptchaInfo;
|
||||
import com.youlai.boot.shared.auth.service.AuthService;
|
||||
import com.youlai.boot.core.security.token.TokenManager;
|
||||
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.shared.sms.service.SmsService;
|
||||
import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.platform.sms.service.SmsService;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken;
|
||||
import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
@@ -87,16 +88,16 @@ public class AuthServiceImpl implements AuthService {
|
||||
@Override
|
||||
public AuthenticationToken loginByWechat(String code) {
|
||||
// 1. 创建用户微信认证的令牌(未认证)
|
||||
WechatAuthenticationToken wechatAuthenticationToken = new WechatAuthenticationToken(code);
|
||||
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(code);
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
Authentication authentication = authenticationManager.authenticate(wechatAuthenticationToken);
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
return authenticationToken;
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +168,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
* @return 验证码
|
||||
*/
|
||||
@Override
|
||||
public CaptchaInfo getCaptcha() {
|
||||
public CaptchaVO getCaptcha() {
|
||||
|
||||
String captchaType = captchaProperties.getType();
|
||||
int width = captchaProperties.getWidth();
|
||||
@@ -203,7 +204,7 @@ public class AuthServiceImpl implements AuthService {
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
|
||||
return CaptchaInfo.builder()
|
||||
return CaptchaVO.builder()
|
||||
.captchaKey(captchaKey)
|
||||
.captchaBase64(imageBase64Data)
|
||||
.build();
|
||||
@@ -217,15 +218,53 @@ public class AuthServiceImpl implements AuthService {
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
// 验证刷新令牌
|
||||
boolean isValidate = tokenManager.validateRefreshToken(refreshToken);
|
||||
|
||||
if (!isValidate) {
|
||||
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
// 刷新令牌有效,生成新的访问令牌
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 令牌类型
|
||||
*/
|
||||
String TOKEN_TYPE = "tokenType";
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.shared.auth.enums;
|
||||
package com.youlai.boot.common.enums;
|
||||
|
||||
/**
|
||||
* 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.inner.DataPermissionInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.youlai.boot.core.handler.MyDataPermissionHandler;
|
||||
import com.youlai.boot.core.handler.MyMetaObjectHandler;
|
||||
import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler;
|
||||
import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
@@ -5,14 +5,15 @@ import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.youlai.boot.config.property.SecurityProperties;
|
||||
import com.youlai.boot.core.filter.RateLimiterFilter;
|
||||
import com.youlai.boot.core.security.exception.MyAccessDeniedHandler;
|
||||
import com.youlai.boot.core.security.exception.MyAuthenticationEntryPoint;
|
||||
import com.youlai.boot.core.security.extension.sms.SmsAuthenticationProvider;
|
||||
import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationProvider;
|
||||
import com.youlai.boot.core.security.filter.CaptchaValidationFilter;
|
||||
import com.youlai.boot.core.security.filter.TokenAuthenticationFilter;
|
||||
import com.youlai.boot.core.security.token.TokenManager;
|
||||
import com.youlai.boot.core.security.service.SysUserDetailsService;
|
||||
import com.youlai.boot.security.filter.CaptchaValidationFilter;
|
||||
import com.youlai.boot.security.filter.TokenAuthenticationFilter;
|
||||
import com.youlai.boot.security.handler.MyAccessDeniedHandler;
|
||||
import com.youlai.boot.security.handler.MyAuthenticationEntryPoint;
|
||||
import com.youlai.boot.security.provider.SmsAuthenticationProvider;
|
||||
import com.youlai.boot.security.provider.WxMiniAppCodeAuthenticationProvider;
|
||||
import com.youlai.boot.security.provider.WxMiniAppPhoneAuthenticationProvider;
|
||||
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.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -118,20 +119,26 @@ public class SecurityConfig {
|
||||
*/
|
||||
@Bean
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
|
||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService);
|
||||
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
|
||||
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
|
||||
return daoAuthenticationProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信认证 Provider
|
||||
* 微信小程序Code认证Provider
|
||||
*/
|
||||
@Bean
|
||||
public WechatAuthenticationProvider weChatAuthenticationProvider() {
|
||||
return new WechatAuthenticationProvider(userService, wxMaService);
|
||||
public WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider() {
|
||||
return new WxMiniAppCodeAuthenticationProvider(userService, wxMaService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序手机号认证Provider
|
||||
*/
|
||||
@Bean
|
||||
public WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider() {
|
||||
return new WxMiniAppPhoneAuthenticationProvider(userService, wxMaService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Provider
|
||||
@@ -147,12 +154,14 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
DaoAuthenticationProvider daoAuthenticationProvider,
|
||||
WechatAuthenticationProvider weChatAuthenticationProvider,
|
||||
WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider,
|
||||
WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider,
|
||||
SmsAuthenticationProvider smsAuthenticationProvider
|
||||
) {
|
||||
return new ProviderManager(
|
||||
daoAuthenticationProvider,
|
||||
weChatAuthenticationProvider,
|
||||
wxMiniAppCodeAuthenticationProvider,
|
||||
wxMiniAppPhoneAuthenticationProvider,
|
||||
smsAuthenticationProvider
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
<<<<<<< HEAD
|
||||
import com.youlai.boot.core.security.model.SysUserDetails;
|
||||
import com.youlai.boot.core.security.token.TokenManager;
|
||||
import com.youlai.boot.system.service.UserOnlineService;
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.messaging.Message;
|
||||
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;
|
||||
|
||||
/**
|
||||
<<<<<<< HEAD
|
||||
* WebSocket配置
|
||||
*
|
||||
* @author You Lai
|
||||
=======
|
||||
* WebSocket 配置类
|
||||
*
|
||||
* 核心功能:
|
||||
* - 配置 WebSocket 端点
|
||||
* - 配置消息代理
|
||||
* - 实现连接认证与授权
|
||||
* - 管理用户会话生命周期
|
||||
*
|
||||
* @author Ray.Hao
|
||||
>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@EnableWebSocketMessageBroker
|
||||
@@ -38,15 +57,34 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
<<<<<<< HEAD
|
||||
private final TokenManager tokenManager;
|
||||
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
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
<<<<<<< HEAD
|
||||
// 注册 /ws 的端点
|
||||
.addEndpoint("/ws")
|
||||
// 允许跨域
|
||||
@@ -54,31 +92,53 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
// 开启SockJS支持,用于不支持WebSocket的浏览器
|
||||
.withSockJS();
|
||||
}
|
||||
=======
|
||||
.addEndpoint(WS_ENDPOINT)
|
||||
.setAllowedOriginPatterns("*"); // 允许跨域(生产环境建议配置具体域名)
|
||||
>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d
|
||||
|
||||
log.info("✓ STOMP 端点已注册: {}", WS_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置消息代理
|
||||
*
|
||||
* - /app 前缀:客户端发送消息到服务端的前缀
|
||||
* - /topic 前缀:用于广播消息
|
||||
* - /queue 前缀:用于点对点消息
|
||||
* - /user 前缀:服务端发送给特定用户的消息前缀
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
// 客户端发送消息的请求前缀
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
registry.setApplicationDestinationPrefixes(APP_DESTINATION_PREFIX);
|
||||
|
||||
<<<<<<< HEAD
|
||||
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
|
||||
registry.enableSimpleBroker("/topic", "/queue");
|
||||
|
||||
// 服务端通知客户端的前缀,可以不设置,默认为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. 连接建立时解析令牌并绑定用户身份
|
||||
* 2. 连接关闭时触发下线通知
|
||||
* 3. 异常Token的防御性处理
|
||||
* 1. 连接建立时:解析 JWT Token 并绑定用户身份
|
||||
* 2. 连接关闭时:触发用户下线通知
|
||||
* 3. 安全防护:拦截无效连接请求
|
||||
*/
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
@@ -86,11 +146,20 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
@Override
|
||||
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
|
||||
// 防御性检查:确保 accessor 不为空
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
<<<<<<< HEAD
|
||||
// 处理客户端连接请求
|
||||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
/*
|
||||
@@ -149,19 +218,178 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
// 记录用户下线状态
|
||||
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) {
|
||||
// 认证失败时强制关闭连接
|
||||
log.error("连接认证失败:{}", ex.getMessage());
|
||||
log.error("❌ 连接认证失败: {}", ex.getMessage());
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
// 捕获其他未知异常
|
||||
log.error("WebSocket连接处理异常:", ex);
|
||||
throw new MessagingException("Connection processing failed");
|
||||
log.error("❌ WebSocket 消息处理异常", ex);
|
||||
throw new MessagingException("消息处理失败: " + ex.getMessage());
|
||||
}
|
||||
|
||||
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.youlai.boot.common.enums.LogModuleEnum;
|
||||
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.service.LogService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -60,6 +60,9 @@ public class LogAspect {
|
||||
*/
|
||||
@Around("logPointcut() && @annotation(logAnnotation)")
|
||||
public Object doAround(ProceedingJoinPoint joinPoint, com.youlai.boot.common.annotation.Log logAnnotation) throws Throwable {
|
||||
// 在方法执行前获取用户ID,避免在方法执行过程中清除上下文导致获取不到用户ID
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
|
||||
TimeInterval timer = DateUtil.timer();
|
||||
Object result = null;
|
||||
Exception exception = null;
|
||||
@@ -71,7 +74,7 @@ public class LogAspect {
|
||||
throw e;
|
||||
} finally {
|
||||
long executionTime = timer.interval(); // 执行时长
|
||||
this.saveLog(joinPoint, exception, result, logAnnotation, executionTime);
|
||||
this.saveLog(joinPoint, exception, result, logAnnotation, executionTime, userId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -84,8 +87,9 @@ public class LogAspect {
|
||||
* @param e 异常
|
||||
* @param jsonResult 响应结果
|
||||
* @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();
|
||||
// 创建日志记录
|
||||
Log log = new Log();
|
||||
@@ -108,7 +112,6 @@ public class LogAspect {
|
||||
}
|
||||
}
|
||||
log.setRequestUri(requestURI);
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
log.setCreateBy(userId);
|
||||
String ipAddr = IPUtils.getIpAddr(request);
|
||||
if (StrUtil.isNotBlank(ipAddr)) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.common.annotation.RepeatSubmit;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
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 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 com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
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 org.springframework.beans.TypeMismatchException;
|
||||
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.servlet.NoHandlerFoundException;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
|
||||
import java.sql.SQLIntegrityConstraintViolationException;
|
||||
import java.sql.SQLSyntaxErrorException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -192,7 +192,7 @@ public class GlobalExceptionHandler {
|
||||
log.error(e.getMessage(), e);
|
||||
String errorMsg = e.getMessage();
|
||||
if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
|
||||
return Result.failed(ResultCode.ACCESS_UNAUTHORIZED);
|
||||
return Result.failed(ResultCode.DATABASE_ACCESS_DENIED);
|
||||
} else {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
@@ -207,7 +207,20 @@ public class GlobalExceptionHandler {
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException 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 com.youlai.boot.common.constant.RedisConstants;
|
||||
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.ResponseUtils;
|
||||
import com.youlai.boot.core.web.WebResponseHelper;
|
||||
import com.youlai.boot.system.service.ConfigService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
@@ -88,7 +88,7 @@ public class RateLimiterFilter extends OncePerRequestFilter {
|
||||
// 判断是否限流
|
||||
if (rateLimit(ip)) {
|
||||
// 返回限流错误信息
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
|
||||
WebResponseHelper.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.common.result;
|
||||
package com.youlai.boot.core.web;
|
||||
|
||||
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 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 lombok.Data;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.common.result;
|
||||
package com.youlai.boot.core.web;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -243,12 +243,16 @@ public enum ResultCode implements IResultCode, Serializable {
|
||||
|
||||
TABLE_NOT_EXIST("C0311", "表不存在"),
|
||||
COLUMN_NOT_EXIST("C0312", "列不存在"),
|
||||
DATABASE_EXECUTION_SYNTAX_ERROR("C0313", "数据库执行语法错误"),
|
||||
|
||||
MULTIPLE_SAME_NAME_COLUMNS_IN_MULTI_TABLE_ASSOCIATION("C0321", "多表关联中存在多个相同名称的列"),
|
||||
|
||||
DATABASE_DEADLOCK("C0331", "数据库死锁"),
|
||||
|
||||
PRIMARY_KEY_CONFLICT("C0341", "主键冲突"),
|
||||
INTEGRITY_CONSTRAINT_VIOLATION("C0342", "违反了完整性约束"),
|
||||
|
||||
DATABASE_ACCESS_DENIED("C0351", "演示环境已禁用数据库写入功能,请本地部署修改数据库链接或开启Mock模式进行体验"),
|
||||
|
||||
/** 二级宏观错误码 */
|
||||
THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"),
|
||||
@@ -293,4 +297,4 @@ public enum ResultCode implements IResultCode, Serializable {
|
||||
}
|
||||
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.youlai.boot.common.result.PageResult;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.core.web.PageResult;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.config.property.CodegenProperties;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.shared.codegen.service.CodegenService;
|
||||
import com.youlai.boot.shared.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.shared.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.shared.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.shared.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.platform.codegen.service.CodegenService;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
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.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -82,17 +82,19 @@ public class CodegenController {
|
||||
@Operation(summary = "获取预览生成代码")
|
||||
@GetMapping("/{tableName}/preview")
|
||||
@Log(value = "预览生成代码", module = LogModuleEnum.OTHER)
|
||||
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName) {
|
||||
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName);
|
||||
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName,
|
||||
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) {
|
||||
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName, pageType);
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "下载代码")
|
||||
@GetMapping("/{tableName}/download")
|
||||
@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(",");
|
||||
byte[] data = codegenService.downloadCode(tableNames);
|
||||
byte[] data = codegenService.downloadCode(tableNames, pageType);
|
||||
|
||||
response.reset();
|
||||
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.shared.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.shared.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
@@ -23,6 +23,8 @@ public interface CodegenConverter {
|
||||
@Mapping(source = "genConfig.packageName", target = "packageName")
|
||||
@Mapping(source = "genConfig.entityName", target = "entityName")
|
||||
@Mapping(source = "genConfig.author", target = "author")
|
||||
@Mapping(source = "genConfig.pageType", target = "pageType")
|
||||
@Mapping(source = "genConfig.removeTablePrefix", target = "removeTablePrefix")
|
||||
@Mapping(source = "fieldConfigs", target = "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.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;
|
||||
|
||||
@@ -28,7 +28,8 @@ public enum JavaTypeEnum {
|
||||
DOUBLE("double", "Double", "number"),
|
||||
DECIMAL("decimal", "BigDecimal", "number"),
|
||||
DATE("date", "LocalDate", "Date"),
|
||||
DATETIME("datetime", "LocalDateTime", "Date");
|
||||
DATETIME("datetime", "LocalDateTime", "Date"),
|
||||
TIMESTAMP("timestamp", "LocalDateTime", "Date");
|
||||
|
||||
// 数据库类型
|
||||
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.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.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.shared.codegen.model.bo.ColumnMetaData;
|
||||
import com.youlai.boot.shared.codegen.model.bo.TableMetaData;
|
||||
import com.youlai.boot.shared.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.shared.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.bo.TableMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
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.youlai.boot.shared.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
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.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
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 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;
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -51,4 +51,14 @@ public class GenConfig extends BaseEntity {
|
||||
* 作者
|
||||
*/
|
||||
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.TableName;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import com.youlai.boot.shared.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.shared.codegen.enums.QueryTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.QueryTypeEnum;
|
||||
import lombok.Getter;
|
||||
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.shared.codegen.enums.QueryTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.QueryTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -50,6 +50,12 @@ public class GenConfigForm {
|
||||
@Schema(description = "前端应用名")
|
||||
private String frontendAppName;
|
||||
|
||||
@Schema(description = "页面类型 classic|curd", example = "classic")
|
||||
private String pageType;
|
||||
|
||||
@Schema(description = "要移除的表前缀,如: sys_", example = "sys_")
|
||||
private String removeTablePrefix;
|
||||
|
||||
@Schema(description = "字段配置")
|
||||
@Data
|
||||
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.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 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 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.youlai.boot.shared.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.shared.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.shared.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -29,12 +29,12 @@ public interface CodegenService {
|
||||
* @param tableName 表名
|
||||
* @return
|
||||
*/
|
||||
List<CodegenPreviewVO> getCodegenPreviewData(String tableName);
|
||||
List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType);
|
||||
|
||||
/**
|
||||
* 下载代码
|
||||
* @param tableNames 表名
|
||||
* @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.youlai.boot.shared.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.shared.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
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.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.date.DateUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.template.Template;
|
||||
@@ -10,18 +11,18 @@ import cn.hutool.extra.template.TemplateEngine;
|
||||
import cn.hutool.extra.template.TemplateUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.shared.codegen.service.GenConfigService;
|
||||
import com.youlai.boot.shared.codegen.service.GenFieldConfigService;
|
||||
import com.youlai.boot.shared.codegen.service.CodegenService;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.shared.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.shared.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.shared.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.shared.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.shared.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
||||
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
||||
import com.youlai.boot.platform.codegen.service.CodegenService;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.model.query.TablePageQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -72,7 +73,7 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @return 预览数据
|
||||
*/
|
||||
@Override
|
||||
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName) {
|
||||
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType) {
|
||||
|
||||
List<CodegenPreviewVO> list = new ArrayList<>();
|
||||
|
||||
@@ -124,7 +125,9 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
|
||||
/* 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);
|
||||
|
||||
list.add(previewVO);
|
||||
@@ -146,7 +149,8 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
} else if ("MapperXml".equals(templateName)) {
|
||||
return entityName + "Mapper" + extension;
|
||||
} else if ("API".equals(templateName)) {
|
||||
return StrUtil.toSymbolCase(entityName, '-') + extension;
|
||||
// 生成 user-api.ts 命名
|
||||
return StrUtil.toSymbolCase(entityName, '-') + "-api" + extension;
|
||||
} else if ("VIEW".equals(templateName)) {
|
||||
return "index.vue";
|
||||
}
|
||||
@@ -211,7 +215,7 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @param fieldConfigs 字段配置
|
||||
* @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<>();
|
||||
|
||||
@@ -252,7 +256,12 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
bindMap.put("hasRequiredField", hasRequiredField);
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -264,13 +273,13 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @return 压缩文件字节数组
|
||||
*/
|
||||
@Override
|
||||
public byte[] downloadCode(String[] tableNames) {
|
||||
public byte[] downloadCode(String[] tableNames, String ui) {
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ZipOutputStream zip = new ZipOutputStream(outputStream)) {
|
||||
|
||||
// 遍历每个表名,生成对应的代码并压缩到 zip 文件中
|
||||
for (String tableName : tableNames) {
|
||||
generateAndZipCode(tableName, zip);
|
||||
generateAndZipCode(tableName, zip, ui);
|
||||
}
|
||||
// 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整
|
||||
zip.finish();
|
||||
@@ -288,8 +297,8 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @param tableName 表名
|
||||
* @param zip 压缩文件输出流
|
||||
*/
|
||||
private void generateAndZipCode(String tableName, ZipOutputStream zip) {
|
||||
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName);
|
||||
private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui) {
|
||||
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName, ui);
|
||||
|
||||
for (CodegenPreviewVO codePreview : codePreviewList) {
|
||||
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.lang.Assert;
|
||||
@@ -7,21 +7,21 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.YouLaiBootApplication;
|
||||
import com.youlai.boot.common.enums.EnvEnum;
|
||||
import com.youlai.boot.shared.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.shared.codegen.enums.JavaTypeEnum;
|
||||
import com.youlai.boot.shared.codegen.enums.QueryTypeEnum;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.platform.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.JavaTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.QueryTypeEnum;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.config.property.CodegenProperties;
|
||||
import com.youlai.boot.shared.codegen.converter.CodegenConverter;
|
||||
import com.youlai.boot.shared.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.shared.codegen.mapper.GenConfigMapper;
|
||||
import com.youlai.boot.shared.codegen.model.bo.ColumnMetaData;
|
||||
import com.youlai.boot.shared.codegen.model.bo.TableMetaData;
|
||||
import com.youlai.boot.shared.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.shared.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.shared.codegen.service.GenConfigService;
|
||||
import com.youlai.boot.shared.codegen.service.GenFieldConfigService;
|
||||
import com.youlai.boot.platform.codegen.converter.CodegenConverter;
|
||||
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.platform.codegen.mapper.GenConfigMapper;
|
||||
import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.bo.TableMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
||||
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
||||
import com.youlai.boot.system.service.MenuService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -83,8 +83,13 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
||||
if (StrUtil.isNotBlank(tableComment)) {
|
||||
genConfig.setBusinessName(tableComment.replace("表", "").trim());
|
||||
}
|
||||
// 根据表名生成实体类名 例如:sys_user -> SysUser
|
||||
genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName))));
|
||||
// 根据表名生成实体类名,支持去除前缀 例如:sys_user -> SysUser
|
||||
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.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.youlai.boot.shared.codegen.mapper.GenFieldConfigMapper;
|
||||
import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.shared.codegen.service.GenFieldConfigService;
|
||||
import com.youlai.boot.platform.codegen.mapper.GenFieldConfigMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
||||
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.shared.file.service.FileService;
|
||||
import com.youlai.boot.shared.file.model.FileInfo;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.platform.file.service.FileService;
|
||||
import com.youlai.boot.platform.file.model.FileInfo;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
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 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;
|
||||
|
||||
/**
|
||||
@@ -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.io.FileUtil;
|
||||
@@ -8,8 +8,8 @@ import com.aliyun.oss.OSS;
|
||||
import com.aliyun.oss.OSSClientBuilder;
|
||||
import com.aliyun.oss.model.ObjectMetadata;
|
||||
import com.aliyun.oss.model.PutObjectRequest;
|
||||
import com.youlai.boot.shared.file.service.FileService;
|
||||
import com.youlai.boot.shared.file.model.FileInfo;
|
||||
import com.youlai.boot.platform.file.service.FileService;
|
||||
import com.youlai.boot.platform.file.model.FileInfo;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.Data;
|
||||
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.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.youlai.boot.shared.file.model.FileInfo;
|
||||
import com.youlai.boot.shared.file.service.FileService;
|
||||
import com.youlai.boot.platform.file.model.FileInfo;
|
||||
import com.youlai.boot.platform.file.service.FileService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.shared.file.model.FileInfo;
|
||||
import com.youlai.boot.shared.file.service.FileService;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.platform.file.model.FileInfo;
|
||||
import com.youlai.boot.platform.file.service.FileService;
|
||||
import io.minio.*;
|
||||
import io.minio.http.Method;
|
||||
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.*;
|
||||
|
||||
/**
|
||||
* 邮件控制层
|
||||
*
|
||||
* @author Ray
|
||||
* @author Ray.Hao
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@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.shared.mail.service.MailService;
|
||||
import com.youlai.boot.platform.mail.service.MailService;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
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 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;
|
||||
|
||||
@@ -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 com.aliyuncs.CommonRequest;
|
||||
@@ -9,8 +9,8 @@ import com.aliyuncs.exceptions.ClientException;
|
||||
import com.aliyuncs.http.MethodType;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.youlai.boot.config.property.AliyunSmsProperties;
|
||||
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.shared.sms.service.SmsService;
|
||||
import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.platform.sms.service.SmsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.extern.slf4j.Slf4j;
|
||||
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.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 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.base.IBaseEnum;
|
||||
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.extern.slf4j.Slf4j;
|
||||
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 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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user