wip:临时提交

This commit is contained in:
Ray.Hao
2025-11-15 15:57:03 +08:00
171 changed files with 4280 additions and 1078 deletions

3
.gitignore vendored
View File

@@ -15,4 +15,5 @@ logs
docker/*/data/
docker/minio/config
docker/xxljob/logs
docker/xxljob/logs
application-youlai.yml

View File

@@ -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

View File

@@ -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>
![](https://foruda.gitee.com/images/1708618984641188532/a7cca095_716974.png "rainbow.png")
<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

View File

@@ -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

View File

@@ -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
View File

@@ -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>

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
/**
* 发送短信验证码
*

View File

@@ -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;
}
}

View File

@@ -10,6 +10,11 @@ package com.youlai.boot.common.constant;
*/
public interface JwtClaimConstants {
/**
* 令牌类型
*/
String TOKEN_TYPE = "tokenType";
/**
* 用户ID
*/

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.shared.auth.enums;
package com.youlai.boot.common.enums;
/**
* EasyCaptcha 验证码类型枚举

View File

@@ -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();
};
}
}

View File

@@ -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;

View File

@@ -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
);
}

View File

@@ -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
}

View File

@@ -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)) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
/**

View File

@@ -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;
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.common.result;
package com.youlai.boot.core.web;
import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.common.result;
package com.youlai.boot.core.web;
/**
* 响应码接口

View File

@@ -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;

View File

@@ -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;

View File

@@ -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; // 默认系统执行错误
}
}
}

View File

@@ -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();
};
}
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.member.controller;
/**
* 会员控制层-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class MemberController {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.member.mapper;
/**
* 会员数据访问层-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class MemberMapper {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.member.model;
/**
* 会员实体-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class Member {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.member.service;
/**
* 会员管理服务类-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class MemberService {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.order.controller;
/**
* 订单控制层-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class OrderController {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.order.mapper;
/**
* 订单数据访问层-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class OrderMapper {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.order.model;
/**
* 订单实体-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class Order {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.order.service;
/**
* 订单管理服务类-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class OrderService {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.product.controller;
/**
* 商品控制层-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class ProductController {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.product.mapper;
/**
* 商品数据访问层-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class ProductMapper {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.product.model;
/**
* 商品实体-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class Product {
}

View File

@@ -1,10 +0,0 @@
package com.youlai.boot.modules.product.service;
/**
* 会员管理服务类-业务模块演示
*
* @author haoxr
* @since 2024/10/10
*/
public class ProductService {
}

View File

@@ -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);
}
}

View File

@@ -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("撤销成功");
}
}

View File

@@ -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> {
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -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";
}
}

View File

@@ -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() : "阿里通义千问";
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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));

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.shared.codegen.model.bo;
package com.youlai.boot.platform.codegen.model.bo;
import lombok.Data;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
/**
* 代码生成配置接口

View File

@@ -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;
/**
* 代码生成配置接口

View File

@@ -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();

View File

@@ -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()); // 默认模块名

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
/**

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.shared.mail.service;
package com.youlai.boot.platform.mail.service;
/**
* 邮件服务接口层

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.shared.sms.controller;
package com.youlai.boot.platform.sms.controller;
/**
* 短信控制层

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.shared.websocket.model;
package com.youlai.boot.platform.websocket.model;
import lombok.AllArgsConstructor;
import lombok.Data;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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