Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5476a4b0b7 | ||
|
|
cd490aa0e5 | ||
|
|
b1ff44df4b | ||
|
|
dc6d00f0fc | ||
|
|
990da8da6f | ||
|
|
8fc7ad0359 | ||
|
|
8a89d9eb9c | ||
|
|
abfa9ae97b | ||
|
|
bde2e8ff8e | ||
|
|
4d8eb1850f | ||
|
|
5aaa3a5662 | ||
|
|
8fec96f5b2 | ||
|
|
15c306eca2 | ||
|
|
620ea1fc76 | ||
|
|
c5c375dc6d | ||
|
|
1b793e822a | ||
|
|
6281840f36 | ||
|
|
f5daa7eb78 | ||
|
|
52cb563383 | ||
|
|
69efc3261e | ||
|
|
fb19fb29cc | ||
|
|
9f257cf712 | ||
|
|
4af26fe4b4 | ||
|
|
ab2a118ee9 | ||
|
|
9cd97a4dc5 | ||
|
|
788b372e32 | ||
|
|
744f9b6c7f | ||
|
|
761d954ef1 | ||
|
|
d1006f50ad | ||
|
|
f24ff5bbdd | ||
|
|
a4314dbbde | ||
|
|
188dc1e55e | ||
|
|
731352fd04 | ||
|
|
4b53939002 | ||
|
|
b50c15755d | ||
|
|
0dc6262b39 | ||
|
|
9b32e3b3b2 | ||
|
|
37a8b7dad3 | ||
|
|
efeb0bd6fb | ||
|
|
af33040117 | ||
|
|
ac4c037634 | ||
|
|
9734ec53f7 | ||
|
|
2b8a92c7d6 | ||
|
|
adb4538317 | ||
|
|
2509099146 | ||
|
|
3be9005f95 | ||
|
|
be6d027cad | ||
|
|
3d679f8749 | ||
|
|
5a5a48e153 | ||
|
|
e5da648941 | ||
|
|
d2755f00bc | ||
|
|
09abc0b5af | ||
|
|
00f362acf1 | ||
|
|
65d479458e | ||
|
|
57e17e0dda | ||
|
|
691d1735fc | ||
|
|
360984bc4b | ||
|
|
0153f004f4 | ||
|
|
cc23508527 | ||
|
|
c884f4f2d3 | ||
|
|
fab6de1f5c | ||
|
|
c02f66636d | ||
|
|
c1162148b1 | ||
|
|
f76fdbf3ad | ||
|
|
feca08b3ec | ||
|
|
72675b17c4 | ||
|
|
9ea5186f49 | ||
|
|
d0a2eadc38 |
142
README.md
@@ -36,48 +36,85 @@
|
||||
|
||||
## 目录
|
||||
|
||||
- [系统体验](#系统体验)
|
||||
- [源码地址](#源码地址)
|
||||
- [特色功能](#特色功能)
|
||||
- [配套文档](#项目文档)
|
||||
- [核心功能](#核心功能)
|
||||
- [项目演示](#项目演示)
|
||||
- [后台管理](#后台管理)
|
||||
- [管理端](#管理端)
|
||||
- [用户端](#用户端)
|
||||
- [小程序端](#小程序端)
|
||||
- [开发前的配置要求](#开发前的配置要求)
|
||||
- [文件目录说明](#文件目录说明)
|
||||
- [使用到的框架](#使用到的框架)
|
||||
- [开发环境](#开发环境)
|
||||
- [项目结构](#项目结构)
|
||||
- [ruoyi-ai](#ruoyi-ai)
|
||||
- [注意事项](#注意事项)
|
||||
- [vben模板](#vben模板)
|
||||
- [贡献者](#贡献者)
|
||||
- [如何参与开源项目](#如何参与开源项目)
|
||||
- [版本控制](#版本控制)
|
||||
- [作者](#作者)
|
||||
- [鸣谢](#鸣谢)
|
||||
- [技术讨论群](#技术讨论群)
|
||||
|
||||
### 系统体验
|
||||
- 用户端:https://web.pandarobot.chat
|
||||
- 管理端:https://admin.pandarobot.chat
|
||||
|
||||
用户名: admin 密码:admin123
|
||||
|
||||
### 源码地址
|
||||
- 项目文档: https://doc.pandarobot.chat
|
||||
- 前端-后台管理: https://github.com/ageerle/ruoyi-admin
|
||||
- 前端-用户端: https://github.com/ageerle/ruoyi-web
|
||||
- 小程序端: https://github.com/ageerle/ruoyi-uniapp
|
||||
- 演示地址: https://web.pandarobot.chat
|
||||
- 后台管理: https://admin.pandarobot.chat
|
||||
- 用户名: admin 密码:admin123
|
||||
|
||||
### gitcode源码地址
|
||||
- https://gitcode.com/ageerle/ruoyi-ai
|
||||
- https://gitcode.com/ageerle/ruoyi-web
|
||||
- https://gitcode.com/ageerle/ruoyi-admin
|
||||
- https://gitcode.com/ageerle/ruoyi-uniapp
|
||||
[1]gitee
|
||||
- 前端服务-用户端: https://gitee.com/ageerle/ruoyi-web
|
||||
- 前端服务-管理端: https://gitee.com/ageerle/ruoyi-admin
|
||||
- 前端服务-小程序端: https://gitee.com/ageerle/ruoyi-uniapp
|
||||
- 后端服务:https://gitee.com/ageerle/ruoyi-ai
|
||||
|
||||
### 特色功能
|
||||
[2]github
|
||||
- 前端服务-用户端: https://github.com/ageerle/ruoyi-web
|
||||
- 前端服务-管理端: https://github.com/ageerle/ruoyi-admin
|
||||
- 前端服务-小程序端: https://github.com/ageerle/ruoyi-uniapp
|
||||
- 后端服务:https://github.com/ageerle/ruoyi-ai
|
||||
|
||||
[3]gitcode
|
||||
- 前端服务-用户端:https://gitcode.com/ageerle/ruoyi-web
|
||||
- 前端服务-管理端: https://gitcode.com/ageerle/ruoyi-admin
|
||||
- 前端服务-小程序端: https://gitcode.com/ageerle/ruoyi-uniapp
|
||||
- 后端服务:https://gitcode.com/ageerle/ruoyi-ai
|
||||
|
||||
### 配套文档
|
||||
- 配套文档: https://doc.pandarobot.chat
|
||||
- 项目部署文档:https://doc.pandarobot.chat/guide/introduction/
|
||||
|
||||
### 核心功能
|
||||
1. 全套开源系统:提供完整的前端应用、后台管理以及小程序应用,基于MIT协议,开箱即用。
|
||||
2. 本地RAG方案:集成Milvus/Weaviate向量库、本地向量化模型与Ollama,实现本地化RAG
|
||||
2. 本地RAG方案:集成Milvus/Weaviate向量库、本地向量化模型与Ollama,实现本地化RAG。
|
||||
3. 丰富插件功能:支持联网、SQL查询插件及Text2API插件,扩展系统能力与应用场景。
|
||||
4. 内置SSE、websocket等网络协议,支持对接多种大语言模型,同时还集成了MidJourney和DALLE AI绘画功能
|
||||
5. 强大的多媒体功能:支持AI翻译、PPT制作、语音克隆和翻唱等
|
||||
6. 扩展功能:支持将大模型接入个人或企业微信
|
||||
7. 支付功能:支持易支付、微信支付等多种支付方式
|
||||
4. 内置SSE、websocket等网络协议,支持对接多种大语言模型,同时还集成了MidJourney和DALLE AI绘画功能。
|
||||
5. 强大的多媒体功能:支持AI翻译、PPT制作、语音克隆和翻唱等。
|
||||
6. 扩展功能:支持将大模型接入个人或企业微信。
|
||||
7. 支付功能:支持易支付、微信支付等多种支付方式。
|
||||
|
||||
### 项目演示
|
||||
|
||||
#### 后台管理
|
||||
#### mcp支持
|
||||
|
||||
### 如何使用
|
||||
1. ruoyi-admin\src\main\resources\application.yml中mcp.client.enabled改为true
|
||||
2. application.yml中配置openai api-key(用于推理使用那个工具,并构建工具所需参数)
|
||||
3. 启动[ruoyi-mcp-server]
|
||||
4. [mcp-server.json]中配置fileSystem.command(npx本地安装路径)
|
||||
5. 指定fileSystem操作目录(本地必须存在指定的目录)
|
||||
6. 配置search1api.env.SEARCH1API_KEY 申请地址:https://www.search1api.com/
|
||||
7. 详情教程:https://blog.csdn.net/weixin_42416319/article/details/147385808
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
|
||||
<img src="image/mcp-01.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
|
||||
<img src="image/mcp-02.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
|
||||
<img src="image/mcp-03.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
|
||||
<img src="image/mcp-04.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
|
||||
</div>
|
||||
|
||||
#### 管理端
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
|
||||
<img src="image/02.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
|
||||
<img src="image/03.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
|
||||
@@ -100,7 +137,7 @@
|
||||
<img src="image/07.png" alt="drawing" style="width: 320px; height: 600px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
|
||||
</div>
|
||||
|
||||
### 开发前的配置要求
|
||||
### 开发环境
|
||||
|
||||
1. jdk 17
|
||||
2. mysql 5.7、8.0
|
||||
@@ -108,8 +145,14 @@
|
||||
4. maven 3.8+
|
||||
5. nodejs 20+ & pnpm
|
||||
|
||||
### 文件目录说明
|
||||
RuoYi-AI
|
||||
- 附-部署配套视频:https://www.bilibili.com/video/BV1jDXkYWEba
|
||||
|
||||
<div>
|
||||
<img src="image/教程搭建.png" alt="drawing" width="600px" height="300px"/>
|
||||
</div>
|
||||
|
||||
### 项目结构
|
||||
- RuoYi-AI
|
||||
|
||||
```
|
||||
├─ ruoyi-admin // 管理模块
|
||||
@@ -158,6 +201,14 @@ RuoYi-AI
|
||||
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
- vben模板
|
||||
|
||||
|
||||
Q:vben5 的模板默认是没有的吗?
|
||||
|
||||
A:vben模板是收费的 请联系vben-vue-plus作者获取。
|
||||
|
||||
### 版本控制
|
||||
|
||||
该项目使用Git进行版本管理。您可以在repository参看当前可用版本。
|
||||
@@ -170,20 +221,16 @@ RuoYi-AI
|
||||
|
||||
### 项目现状
|
||||
|
||||
目前,项目还处于早期阶段,距离成熟还有很长的路要走。由于个人精力有限,项目的发展速度受到了一定的限制。为了加快项目的进度,我真诚地希望更多人能够参与到项目中来。无论是经验丰富的开发者,还是刚刚入门的小白,我都热烈欢迎你们提交Pull Request(PR)。即使代码修改得很少,或者存在一些错误,都没有关系。我会认真审核每一位贡献者的代码,并和大家一起完善项目。
|
||||
目前,项目还处于早期阶段,距离成熟还有很长的路要走。由于个人精力有限,项目的发展速度受到了一定的限制。为了加快项目的进度,我真诚地希望更多人能够参与到项目中来。无论是经验丰富的开发者,还是刚刚入门的小白,我都热烈欢迎你们提交Pull Request(PR)👏👏👏。即使代码修改得很少,或者存在一些错误,都没有关系。我会认真审核每一位贡献者的代码,并和大家一起完善项目⛽️⛽️⛽️。
|
||||
|
||||
### 开发计划
|
||||
|
||||
- 智能体管理
|
||||
| 主题 | 方向 | 时间节点 |
|
||||
| --- |-----------------------------------|--------|
|
||||
| 前端简化版 | 与element-plus-x框架合作,推出基于该框架的前端简化版 | 2025.5 |
|
||||
| agent2agent | Agent2Agent协议支持 | 2025.6 |
|
||||
| 流程编排 | 通过可视化界面和灵活的配置方式,快速构建AI应用 | 2025.7 |
|
||||
|
||||
通过设置提示词、插件、知识库等,用户可以快速构建一个AI应用。这将极大地简化AI应用的开发流程,降低开发门槛,使更多企业能够轻松地利用AI技术。
|
||||
<div>
|
||||
<img src="image/13.png" alt="drawing" width="600px" height="300px"/>
|
||||
</div>
|
||||
|
||||
- 流程编排
|
||||
|
||||
通过流程编排功能,用户可以将不同的模型按照业务逻辑进行有序连接。这将解决单一模型能力不足的问题,充分发挥多个模型的协同作用,从而更好地满足企业的复杂业务需求。
|
||||
|
||||
- 感谢
|
||||
|
||||
@@ -193,7 +240,7 @@ RuoYi-AI
|
||||
|
||||
#### 如何参与开源项目
|
||||
|
||||
贡献使开源社区成为一个学习、激励和创造的绝佳场所。你所作的任何贡献都是**非常感谢**的。
|
||||
贡献使开源社区成为一个学习、激励和创造的绝佳场所。你所作的任何贡献,我们都非常感谢!🙏
|
||||
|
||||
1. Fork 这个项目
|
||||
2. 创建你的功能分支 (`git checkout -b feature/dev`)
|
||||
@@ -231,4 +278,23 @@ RuoYi-AI
|
||||
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
|
||||
|
||||
|
||||
### 附:技术讨论群
|
||||
|
||||
#### 全面开放,欢迎加入
|
||||
🏠 wx:ruoyi-ai(加人备注:ruoyi-ai)
|
||||
|
||||
🏠 qq:1603234088 (加人备注:ruoyi-ai)
|
||||
|
||||
👏👏👏 ruoyi-ai官方交流1群(qq区):1034554687
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
|
||||
<img src="image/QQ区-官方交流1群.png" alt="drawing" style="width: 400px; height: 400px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
|
||||
</div>
|
||||
|
||||
👏👏👏 ruoyi-ai官方交流4群(微信区):
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
|
||||
<img src="image/WX区-官方交流4群.jpg" alt="drawing" style="width: 400px; height: 400px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
image/QQ区-官方交流1群.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
image/WX区-官方交流4群.jpg
Normal file
|
After Width: | Height: | Size: 310 KiB |
BIN
image/mcp-01.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
image/mcp-02.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
image/mcp-03.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
image/mcp-04.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
image/qq-msg.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
BIN
image/wx-msg.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
image/wx-msg2.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
image/教程搭建.png
Normal file
|
After Width: | Height: | Size: 341 KiB |
88
pom.xml
@@ -14,25 +14,26 @@
|
||||
|
||||
<properties>
|
||||
<revision>1.0.0</revision>
|
||||
<spring-boot.version>3.0.6</spring-boot.version>
|
||||
<spring-boot.version>3.4.4</spring-boot.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>17</java.version>
|
||||
<spring-boot.mybatis>3.0.1</spring-boot.mybatis>
|
||||
<mysql.version>8.0.33</mysql.version>
|
||||
<mybatis.version>3.5.16</mybatis.version>
|
||||
<springdoc.version>2.1.0</springdoc.version>
|
||||
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
|
||||
<poi.version>5.2.3</poi.version>
|
||||
<easyexcel.version>3.2.1</easyexcel.version>
|
||||
<velocity.version>2.3</velocity.version>
|
||||
<satoken.version>1.34.0</satoken.version>
|
||||
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
||||
<mybatis-plus.version>3.5.11</mybatis-plus.version>
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
<hutool.version>5.8.18</hutool.version>
|
||||
<hutool.version>5.8.35</hutool.version>
|
||||
<okhttp.version>4.10.0</okhttp.version>
|
||||
<dynamic-ds.version>4.3.1</dynamic-ds.version>
|
||||
<spring-boot-admin.version>3.0.3</spring-boot-admin.version>
|
||||
<redisson.version>3.20.1</redisson.version>
|
||||
<lock4j.version>2.2.4</lock4j.version>
|
||||
<dynamic-ds.version>3.6.1</dynamic-ds.version>
|
||||
<alibaba-ttl.version>2.14.2</alibaba-ttl.version>
|
||||
<xxl-job.version>2.4.0</xxl-job.version>
|
||||
<mapstruct-plus.version>1.2.1</mapstruct-plus.version>
|
||||
@@ -41,10 +42,6 @@
|
||||
<bouncycastle.version>1.72</bouncycastle.version>
|
||||
<!-- 离线IP地址定位库 -->
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
|
||||
<!-- 临时修复 snakeyaml 漏洞 -->
|
||||
<snakeyaml.version>1.33</snakeyaml.version>
|
||||
|
||||
<!-- OSS 配置 -->
|
||||
<aws-java-sdk-s3.version>1.12.400</aws-java-sdk-s3.version>
|
||||
<!-- SMS 配置 -->
|
||||
@@ -60,6 +57,7 @@
|
||||
<weixin-java-miniapp.version>4.5.0</weixin-java-miniapp.version>
|
||||
<weixin-java-pay.version>4.6.0</weixin-java-pay.version>
|
||||
<weixin-java-cp.version>4.6.0</weixin-java-cp.version>
|
||||
<weixin-java-cp.version>4.6.0</weixin-java-cp.version>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
@@ -96,6 +94,12 @@
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>${mysql.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringBoot的依赖配置-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -194,22 +198,22 @@
|
||||
<version>${satoken.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- dynamic-datasource 多数据源-->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
|
||||
<version>${dynamic-ds.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>${spring-boot.mybatis}</version>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis</artifactId>
|
||||
<version>${mybatis.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
@@ -219,6 +223,13 @@
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- dynamic-datasource 多数据源-->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||
<version>${dynamic-ds.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- sql性能分析插件 -->
|
||||
<dependency>
|
||||
<groupId>p6spy</groupId>
|
||||
@@ -244,18 +255,6 @@
|
||||
<version>${tencent.sms.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>de.codecentric</groupId>-->
|
||||
<!-- <artifactId>spring-boot-admin-starter-server</artifactId>-->
|
||||
<!-- <version>${spring-boot-admin.version}</version>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>de.codecentric</groupId>-->
|
||||
<!-- <artifactId>spring-boot-admin-starter-client</artifactId>-->
|
||||
<!-- <version>${spring-boot-admin.version}</version>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<!--redisson-->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
@@ -281,13 +280,6 @@
|
||||
<version>${alibaba-ttl.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 临时修复 snakeyaml 漏洞 -->
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>${snakeyaml.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 加密包引入 -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
@@ -321,10 +313,21 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-knowledge-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-knowledge</artifactId>
|
||||
<artifactId>ruoyi-chat-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
@@ -336,7 +339,7 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-demo</artifactId>
|
||||
<artifactId>ruoyi-wechat</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
@@ -344,10 +347,13 @@
|
||||
</dependencyManagement>
|
||||
|
||||
<modules>
|
||||
<module>ruoyi-admin</module>
|
||||
<module>ruoyi-common</module>
|
||||
<module>ruoyi-modules</module>
|
||||
<module>ruoyi-modules-api</module>
|
||||
<module>ruoyi-admin</module>
|
||||
<module>ruoyi-extend</module>
|
||||
</modules>
|
||||
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#基础镜像
|
||||
FROM findepi/graalvm:java17-native
|
||||
|
||||
# 设置环境变量
|
||||
ENV LANG C.UTF-8
|
||||
ENV LANGUAGE C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
ENV SERVER_PORT=6039
|
||||
|
||||
MAINTAINER ageerle
|
||||
|
||||
RUN mkdir -p /ruoyi/server/logs \
|
||||
/ruoyi/server/temp \
|
||||
/ruoyi/skywalking/agent
|
||||
|
||||
|
||||
#工作空间
|
||||
WORKDIR /ruoyi/server
|
||||
|
||||
|
||||
|
||||
EXPOSE ${SERVER_PORT}
|
||||
|
||||
ADD ./target/ruoyi-admin.jar ./app.jar
|
||||
|
||||
|
||||
ENTRYPOINT ["java", \
|
||||
"-Djava.security.egd=file:/dev/./urandom", \
|
||||
"-Dserver.port=${SERVER_PORT}", \
|
||||
# 应用名称 如果想区分集群节点监控 改成不同的名称即可
|
||||
# "-Dskywalking.agent.service_name=ruoyi-server", \
|
||||
# "-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar", \
|
||||
"-jar", "app.jar"]
|
||||
|
||||
|
||||
@@ -42,11 +42,6 @@
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-doc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
@@ -57,39 +52,14 @@
|
||||
<artifactId>ruoyi-chat</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-knowledge</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-generator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- demo模块 -->
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-demo</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 添加thumbnailator依赖 -->
|
||||
<dependency>
|
||||
<groupId>net.coobird</groupId>
|
||||
<artifactId>thumbnailator</artifactId>
|
||||
<version>0.4.11</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.ollama4j</groupId>
|
||||
<artifactId>ollama4j</artifactId>
|
||||
<version>1.0.79</version>
|
||||
<scope>compile</scope>
|
||||
<artifactId>ruoyi-wechat</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -2,8 +2,6 @@ package org.ruoyi.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import org.ruoyi.common.core.constant.Constants;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.core.domain.model.EmailLoginBody;
|
||||
@@ -16,18 +14,17 @@ import org.ruoyi.common.core.utils.StreamUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.common.tenant.helper.TenantHelper;
|
||||
import org.ruoyi.system.domain.bo.SysTenantBo;
|
||||
import org.ruoyi.system.domain.vo.LoginTenantVo;
|
||||
import org.ruoyi.system.domain.vo.SysTenantVo;
|
||||
import org.ruoyi.system.domain.vo.TenantListVo;
|
||||
import org.ruoyi.system.service.ISysTenantService;
|
||||
|
||||
import org.ruoyi.system.service.SysLoginService;
|
||||
import org.ruoyi.system.service.SysRegisterService;
|
||||
import org.ruoyi.system.domain.vo.LoginVo;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.ruoyi.system.domain.bo.SysTenantBo;
|
||||
import org.ruoyi.system.domain.vo.LoginTenantVo;
|
||||
import org.ruoyi.system.domain.vo.LoginVo;
|
||||
import org.ruoyi.system.domain.vo.SysTenantVo;
|
||||
import org.ruoyi.system.domain.vo.TenantListVo;
|
||||
import org.ruoyi.system.service.ISysTenantService;
|
||||
import org.ruoyi.system.service.SysLoginService;
|
||||
import org.ruoyi.system.service.SysRegisterService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -50,15 +47,6 @@ public class AuthController {
|
||||
private final SysRegisterService registerService;
|
||||
private final ISysTenantService tenantService;
|
||||
|
||||
|
||||
@PostMapping("/xcxLogin")
|
||||
public R<LoginVo> login(@Validated @RequestBody String xcxCode) throws WxErrorException {
|
||||
|
||||
String openidFromCode = loginService.getOpenidFromCode((String) JSONUtil.parseObj(xcxCode).get("xcxCode"));
|
||||
LoginVo loginVo = loginService.mpLogin(openidFromCode);
|
||||
return R.ok(loginVo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录方法
|
||||
*
|
||||
@@ -75,6 +63,7 @@ public class AuthController {
|
||||
body.getUsername(), body.getPassword(),
|
||||
body.getCode(), body.getUuid());
|
||||
loginVo.setToken(token);
|
||||
// 兼容后台管理登录
|
||||
loginVo.setAccess_token(token);
|
||||
loginVo.setUserInfo(LoginHelper.getLoginUser());
|
||||
return R.ok(loginVo);
|
||||
@@ -97,7 +86,6 @@ public class AuthController {
|
||||
|
||||
/**
|
||||
* 访客登录
|
||||
*
|
||||
* @param loginBody 登录信息
|
||||
* @return token信息
|
||||
*/
|
||||
@@ -136,7 +124,7 @@ public class AuthController {
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public R<Void> register(@Validated @RequestBody RegisterBody user, HttpServletRequest request) {
|
||||
String domainName = request.getServerName();
|
||||
String domainName = request.getServerName();
|
||||
user.setDomainName(domainName);
|
||||
registerService.register(user);
|
||||
return R.ok();
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.ruoyi.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -11,7 +12,6 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* @author Lion Li
|
||||
*/
|
||||
@SaIgnore
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
public class IndexController {
|
||||
|
||||
@@ -20,7 +20,9 @@ public class IndexController {
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String index() {
|
||||
return "RuoYi-AI 启动成功";
|
||||
return "RuoYi AI启动成功!";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
package org.ruoyi.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.ruoyi.common.chat.config.ChatConfig;
|
||||
import org.ruoyi.common.chat.domain.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
|
||||
import org.ruoyi.common.chat.entity.chat.Message;
|
||||
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.core.validate.AddGroup;
|
||||
import org.ruoyi.common.excel.utils.ExcelUtil;
|
||||
import org.ruoyi.common.log.annotation.Log;
|
||||
import org.ruoyi.common.log.enums.BusinessType;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.common.web.core.BaseController;
|
||||
import org.ruoyi.knowledge.domain.bo.KnowledgeAttachBo;
|
||||
import org.ruoyi.knowledge.domain.bo.KnowledgeFragmentBo;
|
||||
import org.ruoyi.knowledge.domain.bo.KnowledgeInfoBo;
|
||||
import org.ruoyi.knowledge.domain.req.KnowledgeInfoUploadRequest;
|
||||
import org.ruoyi.knowledge.domain.vo.KnowledgeAttachVo;
|
||||
import org.ruoyi.knowledge.domain.vo.KnowledgeFragmentVo;
|
||||
import org.ruoyi.knowledge.domain.vo.KnowledgeInfoVo;
|
||||
import org.ruoyi.knowledge.service.EmbeddingService;
|
||||
import org.ruoyi.knowledge.service.IKnowledgeAttachService;
|
||||
import org.ruoyi.knowledge.service.IKnowledgeFragmentService;
|
||||
import org.ruoyi.knowledge.service.IKnowledgeInfoService;
|
||||
import org.ruoyi.system.listener.SSEEventSourceListener;
|
||||
import org.ruoyi.system.service.ISseService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.ruoyi.knowledge.chain.vectorstore.VectorStore;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* 知识库
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2024-10-21
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/knowledge")
|
||||
public class KnowledgeController extends BaseController {
|
||||
|
||||
private final IKnowledgeInfoService knowledgeInfoService;
|
||||
|
||||
private final VectorStore vectorStore;
|
||||
|
||||
private final IKnowledgeAttachService attachService;
|
||||
|
||||
private final IKnowledgeFragmentService fragmentService;
|
||||
|
||||
private final EmbeddingService embeddingService;
|
||||
|
||||
private OpenAiStreamClient openAiStreamClient;
|
||||
|
||||
private final ChatConfig chatConfig;
|
||||
|
||||
private final ISseService sseService;
|
||||
|
||||
/**
|
||||
* 知识库对话
|
||||
*/
|
||||
@PostMapping("/send")
|
||||
public SseEmitter send(@RequestBody @Valid ChatRequest chatRequest) {
|
||||
|
||||
openAiStreamClient = chatConfig.getOpenAiStreamClient();
|
||||
SseEmitter sseEmitter = new SseEmitter(0L);
|
||||
SSEEventSourceListener openAIEventSourceListener = new SSEEventSourceListener(sseEmitter);
|
||||
List<Message> messages = chatRequest.getMessages();
|
||||
String content = messages.get(messages.size() - 1).getContent().toString();
|
||||
List<String> nearestList;
|
||||
List<Double> queryVector = embeddingService.getQueryVector(content, chatRequest.getKid());
|
||||
nearestList = vectorStore.nearest(queryVector,chatRequest.getKid());
|
||||
for (String prompt : nearestList) {
|
||||
Message sysMessage = Message.builder().content(prompt).role(Message.Role.USER).build();
|
||||
messages.add(sysMessage);
|
||||
}
|
||||
Message userMessage = Message.builder().content(content + (nearestList.size() > 0 ? "\n\n注意:回答问题时,须严格根据我给你的系统上下文内容原文进行回答,请不要自己发挥,回答时保持原来文本的段落层级" : "") ).role(Message.Role.USER).build();
|
||||
messages.add(userMessage);
|
||||
if (chatRequest.getModel().startsWith("ollama")) {
|
||||
return sseService.ollamaChat(chatRequest);
|
||||
}
|
||||
|
||||
ChatCompletion completion = ChatCompletion
|
||||
.builder()
|
||||
.messages(messages)
|
||||
.model(chatRequest.getModel())
|
||||
.temperature(chatRequest.getTemperature())
|
||||
.topP(chatRequest.getTop_p())
|
||||
.stream(true)
|
||||
.build();
|
||||
openAiStreamClient.streamChatCompletion(completion, openAIEventSourceListener);
|
||||
|
||||
return sseEmitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户信息查询本地知识库
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<KnowledgeInfoVo> list(KnowledgeInfoBo bo, PageQuery pageQuery) {
|
||||
if(!StpUtil.isLogin()){
|
||||
return null;
|
||||
}
|
||||
bo.setUid(LoginHelper.getUserId());
|
||||
return knowledgeInfoService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增知识库
|
||||
*/
|
||||
@Log(title = "知识库", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/save")
|
||||
public R<Void> save(@Validated(AddGroup.class) @RequestBody KnowledgeInfoBo bo) {
|
||||
knowledgeInfoService.saveOne(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识库
|
||||
*/
|
||||
@PostMapping("/remove/{id}")
|
||||
public R<String> remove(@PathVariable String id){
|
||||
knowledgeInfoService.removeKnowledge(id);
|
||||
return R.ok("删除知识库成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改知识库
|
||||
*/
|
||||
@Log(title = "知识库", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/edit")
|
||||
public R<Void> edit( @RequestBody KnowledgeInfoBo bo) {
|
||||
return toAjax(knowledgeInfoService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出知识库列表
|
||||
*/
|
||||
@Log(title = "知识库", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(KnowledgeInfoBo bo, HttpServletResponse response) {
|
||||
List<KnowledgeInfoVo> list = knowledgeInfoService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "知识库", KnowledgeInfoVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询知识附件信息
|
||||
*/
|
||||
@GetMapping("/detail/{kid}")
|
||||
public TableDataInfo<KnowledgeAttachVo> attach(KnowledgeAttachBo bo, PageQuery pageQuery,@PathVariable String kid){
|
||||
bo.setKid(kid);
|
||||
return attachService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传知识库附件
|
||||
*/
|
||||
@PostMapping(value = "/attach/upload")
|
||||
public R<String> upload(KnowledgeInfoUploadRequest request){
|
||||
knowledgeInfoService.upload(request);
|
||||
return R.ok("上传知识库附件成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库附件详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@GetMapping("attach/info/{id}")
|
||||
public R<KnowledgeAttachVo> getAttachInfo(@NotNull(message = "主键不能为空")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(attachService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识库附件
|
||||
*
|
||||
*/
|
||||
@PostMapping("attach/remove/{docId}")
|
||||
public R<Void> removeAttach(@NotEmpty(message = "主键不能为空") @PathVariable String docId) {
|
||||
attachService.removeKnowledgeAttach(docId);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询知识片段
|
||||
*/
|
||||
@GetMapping("/fragment/list/{docId}")
|
||||
public TableDataInfo<KnowledgeFragmentVo> fragmentList(KnowledgeFragmentBo bo, PageQuery pageQuery, @PathVariable String docId) {
|
||||
bo.setDocId(docId);
|
||||
return fragmentService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,10 +25,9 @@ spring:
|
||||
master:
|
||||
type: ${spring.datasource.type}
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://43.139.70.230:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
||||
username: ruoyi-ai
|
||||
password: ruoyi-ai
|
||||
|
||||
url: jdbc:mysql://127.0.0.1:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
|
||||
username: ry-vue
|
||||
password: xx
|
||||
|
||||
hikari:
|
||||
# 最大连接池数量
|
||||
@@ -61,9 +60,6 @@ spring.data:
|
||||
# password: 123456
|
||||
# 连接超时时间
|
||||
timeout: 10S
|
||||
# 是否开启ssl
|
||||
ssl: false
|
||||
|
||||
redisson:
|
||||
# redis key前缀
|
||||
keyPrefix:
|
||||
@@ -97,3 +93,4 @@ sms:
|
||||
signName: 测试
|
||||
# 腾讯专用
|
||||
sdkAppId:
|
||||
|
||||
|
||||
@@ -49,8 +49,10 @@ server:
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
org.ruoyi: @logging.level@
|
||||
org.dromara: @logging.level@
|
||||
org.springframework: warn
|
||||
org.mybatis.spring.mapper: error
|
||||
org.apache.fury: warn
|
||||
config: classpath:logback-plus.xml
|
||||
|
||||
# 用户配置
|
||||
@@ -318,5 +320,20 @@ wechat:
|
||||
token: ''
|
||||
aesKey: ''
|
||||
|
||||
|
||||
spring:
|
||||
ai:
|
||||
openai:
|
||||
api-key: sk-xx
|
||||
base-url: https://api.pandarobot.chat/
|
||||
mcp:
|
||||
client:
|
||||
enabled: false
|
||||
name: ruoyi-ai-mcp
|
||||
sse:
|
||||
connections:
|
||||
server:
|
||||
url: http://127.0.0.1:8081
|
||||
stdio:
|
||||
servers-configuration: classpath:mcp-server.json
|
||||
request-timeout: 300s
|
||||
|
||||
|
||||
22
ruoyi-admin/src/main/resources/mcp-server.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"fileSystem": {
|
||||
"command": "C:\\Program Files\\nodejs\\npx.cmd",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
"D:\\"
|
||||
]
|
||||
},
|
||||
"search1api": {
|
||||
"command": "C:\\Program Files\\nodejs\\npx.cmd",
|
||||
"args": [
|
||||
"-y",
|
||||
"search1api-mcp"
|
||||
],
|
||||
"env": {
|
||||
"SEARCH1API_KEY": "xx"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,20 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>ruoyi-ai</artifactId>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<description>
|
||||
common 通用模块
|
||||
</description>
|
||||
|
||||
<modules>
|
||||
<module>ruoyi-common-bom</module>
|
||||
@@ -32,16 +39,9 @@
|
||||
<module>ruoyi-common-encrypt</module>
|
||||
<module>ruoyi-common-tenant</module>
|
||||
<module>ruoyi-common-chat</module>
|
||||
<module>ruoyi-common-pay</module>
|
||||
<module>ruoyi-common-wechat</module>
|
||||
<module>ruoyi-common-pay</module>
|
||||
<module>ruoyi-common-live</module>
|
||||
</modules>
|
||||
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<description>
|
||||
common 通用模块
|
||||
</description>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -159,29 +159,14 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 微信模块 -->
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-wechat</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- AI绘画 -->
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-chat</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<!-- 支付模块 -->
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-pay</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</dependencyManagement>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
|
||||
<properties>
|
||||
<retrofit2.version>2.9.0</retrofit2.version>
|
||||
<azure.version>1.0.0-beta.12</azure.version>
|
||||
<chatglm.version>release-V4-2.3.0</chatglm.version>
|
||||
<okhttp.version>2.7.5</okhttp.version>
|
||||
<jtokkit.version>0.5.0</jtokkit.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -26,38 +30,22 @@
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 序列化模块 -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.33</version>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-json</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- redis模块 -->
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.azure</groupId>
|
||||
<artifactId>azure-ai-openai</artifactId>
|
||||
<version>1.0.0-beta.12</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.ollama4j</groupId>
|
||||
<artifactId>ollama4j</artifactId>
|
||||
<version>1.0.79</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 序列化模块 -->
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-json</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-satoken</artifactId>
|
||||
<version>${azure.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -79,13 +67,7 @@
|
||||
<dependency>
|
||||
<groupId>com.knuddels</groupId>
|
||||
<artifactId>jtokkit</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.12</version>
|
||||
<version>${jtokkit.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -98,21 +80,18 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.bigmodel.openapi</groupId>
|
||||
<artifactId>oapi-java-sdk</artifactId>
|
||||
<version>release-V4-2.3.0</version>
|
||||
<version>${chatglm.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>2.7.5</version>
|
||||
<scope>compile</scope>
|
||||
<version>${okhttp.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -12,6 +12,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@Data
|
||||
public class WebSocketProperties {
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package org.ruoyi.common.chat.demo;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okhttp3.sse.EventSource;
|
||||
import okhttp3.sse.EventSourceListener;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
/**
|
||||
* 描述: sse
|
||||
*
|
||||
* @author https:www.unfbx.com
|
||||
* 2023-06-15
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConsoleEventSourceListenerV2 extends EventSourceListener {
|
||||
@Getter
|
||||
String args = "";
|
||||
final CountDownLatch countDownLatch;
|
||||
|
||||
public ConsoleEventSourceListenerV2(CountDownLatch countDownLatch) {
|
||||
this.countDownLatch = countDownLatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(EventSource eventSource, Response response) {
|
||||
log.info("OpenAI建立sse连接...");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(EventSource eventSource, String id, String type, String data) {
|
||||
log.info("OpenAI返回数据:{}", data);
|
||||
if (data.equals("[DONE]")) {
|
||||
log.info("OpenAI返回数据结束了");
|
||||
countDownLatch.countDown();
|
||||
return;
|
||||
}
|
||||
ChatCompletionResponse chatCompletionResponse = JSONUtil.toBean(data, ChatCompletionResponse.class);
|
||||
if(Objects.nonNull(chatCompletionResponse.getChoices().get(0).getDelta().getFunctionCall())){
|
||||
args += chatCompletionResponse.getChoices().get(0).getDelta().getFunctionCall().getArguments();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(EventSource eventSource) {
|
||||
log.info("OpenAI关闭sse连接...");
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void onFailure(EventSource eventSource, Throwable t, Response response) {
|
||||
if(Objects.isNull(response)){
|
||||
log.error("OpenAI sse连接异常:{}", t);
|
||||
eventSource.cancel();
|
||||
return;
|
||||
}
|
||||
ResponseBody body = response.body();
|
||||
if (Objects.nonNull(body)) {
|
||||
log.error("OpenAI sse连接异常data:{},异常:{}", body.string(), t);
|
||||
} else {
|
||||
log.error("OpenAI sse连接异常data:{},异常:{}", response, t);
|
||||
}
|
||||
eventSource.cancel();
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package org.ruoyi.common.chat.demo;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okhttp3.sse.EventSource;
|
||||
import okhttp3.sse.EventSourceListener;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
|
||||
import org.ruoyi.common.chat.entity.chat.Message;
|
||||
import org.ruoyi.common.chat.entity.chat.tool.ToolCallFunction;
|
||||
import org.ruoyi.common.chat.entity.chat.tool.ToolCalls;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
/**
|
||||
* 描述: demo测试实现类,仅供思路参考
|
||||
*
|
||||
* @author https:www.unfbx.com
|
||||
* 2023-11-12
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConsoleEventSourceListenerV3 extends EventSourceListener {
|
||||
@Getter
|
||||
List<ToolCalls> choices = new ArrayList<>();
|
||||
@Getter
|
||||
ToolCalls toolCalls = new ToolCalls();
|
||||
@Getter
|
||||
ToolCallFunction toolCallFunction = ToolCallFunction.builder().name("").arguments("").build();
|
||||
final CountDownLatch countDownLatch;
|
||||
|
||||
public ConsoleEventSourceListenerV3(CountDownLatch countDownLatch) {
|
||||
this.countDownLatch = countDownLatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(EventSource eventSource, Response response) {
|
||||
log.info("OpenAI建立sse连接...");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(EventSource eventSource, String id, String type, String data) {
|
||||
log.info("OpenAI返回数据:{}", data);
|
||||
if (data.equals("[DONE]")) {
|
||||
log.info("OpenAI返回数据结束了");
|
||||
return;
|
||||
}
|
||||
ChatCompletionResponse chatCompletionResponse = JSONUtil.toBean(data, ChatCompletionResponse.class);
|
||||
Message delta = chatCompletionResponse.getChoices().get(0).getDelta();
|
||||
if (CollectionUtil.isNotEmpty(delta.getToolCalls())) {
|
||||
choices.addAll(delta.getToolCalls());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(EventSource eventSource) {
|
||||
if(CollectionUtil.isNotEmpty(choices)){
|
||||
toolCalls.setId(choices.get(0).getId());
|
||||
toolCalls.setType(choices.get(0).getType());
|
||||
choices.forEach(e -> {
|
||||
toolCallFunction.setName(e.getFunction().getName());
|
||||
toolCallFunction.setArguments(toolCallFunction.getArguments() + e.getFunction().getArguments());
|
||||
toolCalls.setFunction(toolCallFunction);
|
||||
});
|
||||
}
|
||||
log.info("OpenAI关闭sse连接...");
|
||||
countDownLatch.countDown();
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void onFailure(EventSource eventSource, Throwable t, Response response) {
|
||||
if(Objects.isNull(response)){
|
||||
log.error("OpenAI sse连接异常:{}", t);
|
||||
eventSource.cancel();
|
||||
return;
|
||||
}
|
||||
ResponseBody body = response.body();
|
||||
if (Objects.nonNull(body)) {
|
||||
log.error("OpenAI sse连接异常data:{},异常:{}", body.string(), t);
|
||||
} else {
|
||||
log.error("OpenAI sse连接异常data:{},异常:{}", response, t);
|
||||
}
|
||||
eventSource.cancel();
|
||||
}
|
||||
}
|
||||
@@ -1,417 +0,0 @@
|
||||
package org.ruoyi.common.chat.demo;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ruoyi.common.chat.entity.chat.*;
|
||||
import org.ruoyi.common.chat.entity.chat.tool.ToolCallFunction;
|
||||
import org.ruoyi.common.chat.entity.chat.tool.ToolCalls;
|
||||
import org.ruoyi.common.chat.entity.chat.tool.Tools;
|
||||
import org.ruoyi.common.chat.entity.chat.tool.ToolsFunction;
|
||||
import org.ruoyi.common.chat.openai.OpenAiClient;
|
||||
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
|
||||
import org.ruoyi.common.chat.openai.function.KeyRandomStrategy;
|
||||
import org.ruoyi.common.chat.openai.interceptor.DynamicKeyOpenAiAuthInterceptor;
|
||||
import org.ruoyi.common.chat.openai.interceptor.OpenAILogger;
|
||||
import org.ruoyi.common.chat.openai.interceptor.OpenAiResponseInterceptor;
|
||||
import org.ruoyi.common.chat.openai.plugin.PluginAbstract;
|
||||
import org.ruoyi.common.chat.plugin.CmdPlugin;
|
||||
import org.ruoyi.common.chat.plugin.CmdReq;
|
||||
import org.ruoyi.common.chat.sse.ConsoleEventSourceListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 描述:
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* date 2025/3/8
|
||||
*/
|
||||
@Slf4j
|
||||
public class PluginTest {
|
||||
|
||||
private OpenAiClient openAiClient;
|
||||
private OpenAiStreamClient openAiStreamClient;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
//可以为null
|
||||
// Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 7890));
|
||||
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
|
||||
//!!!!千万别再生产或者测试环境打开BODY级别日志!!!!
|
||||
//!!!生产或者测试环境建议设置为这三种级别:NONE,BASIC,HEADERS,!!!
|
||||
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
|
||||
OkHttpClient okHttpClient = new OkHttpClient
|
||||
.Builder()
|
||||
// .proxy(proxy)
|
||||
.addInterceptor(httpLoggingInterceptor)
|
||||
.addInterceptor(new OpenAiResponseInterceptor())
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
openAiClient = OpenAiClient.builder()
|
||||
//支持多key传入,请求时候随机选择
|
||||
.apiKey(Arrays.asList("sk-xx"))
|
||||
//自定义key的获取策略:默认KeyRandomStrategy
|
||||
//.keyStrategy(new KeyRandomStrategy())
|
||||
.keyStrategy(new KeyRandomStrategy())
|
||||
.okHttpClient(okHttpClient)
|
||||
//自己做了代理就传代理地址,没有可不不传,(关注公众号回复:openai ,获取免费的测试代理地址)
|
||||
.apiHost("https://api.pandarobot.chat/")
|
||||
.build();
|
||||
|
||||
openAiStreamClient = OpenAiStreamClient.builder()
|
||||
//支持多key传入,请求时候随机选择
|
||||
.apiKey(Arrays.asList("sk-xx"))
|
||||
//自定义key的获取策略:默认KeyRandomStrategy
|
||||
.keyStrategy(new KeyRandomStrategy())
|
||||
.authInterceptor(new DynamicKeyOpenAiAuthInterceptor())
|
||||
.okHttpClient(okHttpClient)
|
||||
//自己做了代理就传代理地址,没有可不不传,(关注公众号回复:openai ,获取免费的测试代理地址)
|
||||
.apiHost("https://api.pandarobot.chat/")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void chatFunction() {
|
||||
//模型:GPT_3_5_TURBO_16K_0613
|
||||
Message message = Message.builder().role(Message.Role.USER).content("给我输出一个长度为2的中文词语,并解释下词语对应物品的用途").build();
|
||||
//属性一
|
||||
JSONObject wordLength = new JSONObject();
|
||||
wordLength.put("type", "number");
|
||||
wordLength.put("description", "词语的长度");
|
||||
//属性二
|
||||
JSONObject language = new JSONObject();
|
||||
language.put("type", "string");
|
||||
language.put("enum", Arrays.asList("zh", "en"));
|
||||
language.put("description", "语言类型,例如:zh代表中文、en代表英语");
|
||||
//参数
|
||||
JSONObject properties = new JSONObject();
|
||||
properties.put("wordLength", wordLength);
|
||||
properties.put("language", language);
|
||||
|
||||
Parameters parameters = Parameters.builder()
|
||||
.type("object")
|
||||
.properties(properties)
|
||||
.required(Collections.singletonList("wordLength")).build();
|
||||
Functions functions = Functions.builder()
|
||||
.name("getOneWord")
|
||||
.description("获取一个指定长度和语言类型的词语")
|
||||
.parameters(parameters)
|
||||
.build();
|
||||
|
||||
ChatCompletion chatCompletion = ChatCompletion
|
||||
.builder()
|
||||
.messages(Collections.singletonList(message))
|
||||
.functions(Collections.singletonList(functions))
|
||||
.functionCall("auto")
|
||||
.model(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
|
||||
.build();
|
||||
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion);
|
||||
|
||||
ChatChoice chatChoice = chatCompletionResponse.getChoices().get(0);
|
||||
log.info("构造的方法值:{}", chatChoice.getMessage().getFunctionCall());
|
||||
log.info("构造的方法名称:{}", chatChoice.getMessage().getFunctionCall().getName());
|
||||
log.info("构造的方法参数:{}", chatChoice.getMessage().getFunctionCall().getArguments());
|
||||
WordParam wordParam = JSONUtil.toBean(chatChoice.getMessage().getFunctionCall().getArguments(), WordParam.class);
|
||||
String oneWord = getOneWord(wordParam);
|
||||
|
||||
FunctionCall functionCall = FunctionCall.builder()
|
||||
.arguments(chatChoice.getMessage().getFunctionCall().getArguments())
|
||||
.name("getOneWord")
|
||||
.build();
|
||||
Message message2 = Message.builder().role(Message.Role.ASSISTANT).content("方法参数").functionCall(functionCall).build();
|
||||
String content
|
||||
= "{ " +
|
||||
"\"wordLength\": \"3\", " +
|
||||
"\"language\": \"zh\", " +
|
||||
"\"word\": \"" + oneWord + "\"," +
|
||||
"\"用途\": [\"直接吃\", \"做沙拉\", \"售卖\"]" +
|
||||
"}";
|
||||
Message message3 = Message.builder().role(Message.Role.FUNCTION).name("getOneWord").content(content).build();
|
||||
List<Message> messageList = Arrays.asList(message, message2, message3);
|
||||
ChatCompletion chatCompletionV2 = ChatCompletion
|
||||
.builder()
|
||||
.messages(messageList)
|
||||
.model(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
|
||||
.build();
|
||||
ChatCompletionResponse chatCompletionResponseV2 = openAiClient.chatCompletion(chatCompletionV2);
|
||||
log.info("自定义的方法返回值:{}",chatCompletionResponseV2.getChoices().get(0).getMessage().getContent());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void plugin() {
|
||||
CmdPlugin plugin = new CmdPlugin(CmdReq.class);
|
||||
// 插件名称
|
||||
plugin.setName("命令行工具");
|
||||
// 方法名称
|
||||
plugin.setFunction("openCmd");
|
||||
// 方法说明
|
||||
plugin.setDescription("提供一个命令行指令,比如<记事本>,指令使用中文,以function返回结果为准");
|
||||
|
||||
PluginAbstract.Arg arg = new PluginAbstract.Arg();
|
||||
// 参数名称
|
||||
arg.setName("cmd");
|
||||
// 参数说明
|
||||
arg.setDescription("命令行指令");
|
||||
// 参数类型
|
||||
arg.setType("string");
|
||||
arg.setRequired(true);
|
||||
plugin.setArgs(Collections.singletonList(arg));
|
||||
|
||||
Message message2 = Message.builder().role(Message.Role.USER).content("帮我打开计算器,结合上下文判断指令是否执行成功,只用回复成功或者失败").build();
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(message2);
|
||||
//有四个重载方法,都可以使用
|
||||
ChatCompletionResponse response = openAiClient.chatCompletionWithPlugin(messages,"gpt-4o-mini",plugin);
|
||||
log.info("自定义的方法返回值:{}", response.getChoices().get(0).getMessage().getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义返回数据格式
|
||||
*/
|
||||
@Test
|
||||
public void diyReturnDataModelChat() {
|
||||
Message message = Message.builder().role(Message.Role.USER).content("随机输出10个单词,使用json输出").build();
|
||||
ChatCompletion chatCompletion = ChatCompletion
|
||||
.builder()
|
||||
.messages(Collections.singletonList(message))
|
||||
.responseFormat(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT.getName()).build())
|
||||
.model(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
|
||||
.build();
|
||||
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion);
|
||||
chatCompletionResponse.getChoices().forEach(e -> System.out.println(e.getMessage()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void streamPlugin() {
|
||||
WeatherPlugin plugin = new WeatherPlugin(WeatherReq.class);
|
||||
plugin.setName("知心天气");
|
||||
plugin.setFunction("getLocationWeather");
|
||||
plugin.setDescription("提供一个地址,方法将会获取该地址的天气的实时温度信息。");
|
||||
PluginAbstract.Arg arg = new PluginAbstract.Arg();
|
||||
arg.setName("location");
|
||||
arg.setDescription("地名");
|
||||
arg.setType("string");
|
||||
arg.setRequired(true);
|
||||
plugin.setArgs(Collections.singletonList(arg));
|
||||
|
||||
// Message message1 = Message.builder().role(Message.Role.USER).content("秦始皇统一了哪六国。").build();
|
||||
Message message2 = Message.builder().role(Message.Role.USER).content("获取上海市的天气现在多少度,然后再给出3个推荐的户外运动。").build();
|
||||
List<Message> messages = new ArrayList<>();
|
||||
// messages.add(message1);
|
||||
messages.add(message2);
|
||||
//默认模型:GPT_3_5_TURBO_16K_0613
|
||||
//有四个重载方法,都可以使用
|
||||
openAiStreamClient.streamChatCompletionWithPlugin(messages, ChatCompletion.Model.GPT_4_1106_PREVIEW.getName(), new ConsoleEventSourceListener(), plugin);
|
||||
CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||
try {
|
||||
countDownLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tools使用示例
|
||||
*/
|
||||
@Test
|
||||
public void toolsChat() {
|
||||
Message message = Message.builder().role(Message.Role.USER).content("给我输出一个长度为2的中文词语,并解释下词语对应物品的用途").build();
|
||||
//属性一
|
||||
JSONObject wordLength = new JSONObject();
|
||||
wordLength.put("type", "number");
|
||||
wordLength.put("description", "词语的长度");
|
||||
//属性二
|
||||
JSONObject language = new JSONObject();
|
||||
language.put("type", "string");
|
||||
language.put("enum", Arrays.asList("zh", "en"));
|
||||
language.put("description", "语言类型,例如:zh代表中文、en代表英语");
|
||||
//参数
|
||||
JSONObject properties = new JSONObject();
|
||||
properties.put("wordLength", wordLength);
|
||||
properties.put("language", language);
|
||||
Parameters parameters = Parameters.builder()
|
||||
.type("object")
|
||||
.properties(properties)
|
||||
.required(Collections.singletonList("wordLength")).build();
|
||||
Tools tools = Tools.builder()
|
||||
.type(Tools.Type.FUNCTION.getName())
|
||||
.function(ToolsFunction.builder().name("getOneWord").description("获取一个指定长度和语言类型的词语").parameters(parameters).build())
|
||||
.build();
|
||||
|
||||
ChatCompletion chatCompletion = ChatCompletion
|
||||
.builder()
|
||||
.messages(Collections.singletonList(message))
|
||||
.tools(Collections.singletonList(tools))
|
||||
.model(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
|
||||
.build();
|
||||
ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion);
|
||||
|
||||
ChatChoice chatChoice = chatCompletionResponse.getChoices().get(0);
|
||||
log.info("构造的方法值:{}", chatChoice.getMessage().getToolCalls());
|
||||
|
||||
ToolCalls openAiReturnToolCalls = chatChoice.getMessage().getToolCalls().get(0);
|
||||
WordParam wordParam = JSONUtil.toBean(openAiReturnToolCalls.getFunction().getArguments(), WordParam.class);
|
||||
String oneWord = getOneWord(wordParam);
|
||||
|
||||
|
||||
ToolCallFunction tcf = ToolCallFunction.builder().name("getOneWord").arguments(openAiReturnToolCalls.getFunction().getArguments()).build();
|
||||
ToolCalls tc = ToolCalls.builder().id(openAiReturnToolCalls.getId()).type(ToolCalls.Type.FUNCTION.getName()).function(tcf).build();
|
||||
//构造tool call
|
||||
Message message2 = Message.builder().role(Message.Role.ASSISTANT).content("方法参数").toolCalls(Collections.singletonList(tc)).build();
|
||||
String content
|
||||
= "{ " +
|
||||
"\"wordLength\": \"3\", " +
|
||||
"\"language\": \"zh\", " +
|
||||
"\"word\": \"" + oneWord + "\"," +
|
||||
"\"用途\": [\"直接吃\", \"做沙拉\", \"售卖\"]" +
|
||||
"}";
|
||||
Message message3 = Message.builder().toolCallId(openAiReturnToolCalls.getId()).role(Message.Role.TOOL).name("getOneWord").content(content).build();
|
||||
List<Message> messageList = Arrays.asList(message, message2, message3);
|
||||
ChatCompletion chatCompletionV2 = ChatCompletion
|
||||
.builder()
|
||||
.messages(messageList)
|
||||
.model(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
|
||||
.build();
|
||||
ChatCompletionResponse chatCompletionResponseV2 = openAiClient.chatCompletion(chatCompletionV2);
|
||||
log.info("自定义的方法返回值:{}", chatCompletionResponseV2.getChoices().get(0).getMessage().getContent());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* tools流式输出使用示例
|
||||
*/
|
||||
@Test
|
||||
public void streamToolsChat() {
|
||||
|
||||
CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||
ConsoleEventSourceListenerV3 eventSourceListener = new ConsoleEventSourceListenerV3(countDownLatch);
|
||||
|
||||
Message message = Message.builder().role(Message.Role.USER).content("给我输出一个长度为2的中文词语,并解释下词语对应物品的用途").build();
|
||||
//属性一
|
||||
JSONObject wordLength = new JSONObject();
|
||||
wordLength.put("type", "number");
|
||||
wordLength.put("description", "词语的长度");
|
||||
//属性二
|
||||
JSONObject language = new JSONObject();
|
||||
language.put("type", "string");
|
||||
language.put("enum", Arrays.asList("zh", "en"));
|
||||
language.put("description", "语言类型,例如:zh代表中文、en代表英语");
|
||||
//参数
|
||||
JSONObject properties = new JSONObject();
|
||||
properties.put("wordLength", wordLength);
|
||||
properties.put("language", language);
|
||||
Parameters parameters = Parameters.builder()
|
||||
.type("object")
|
||||
.properties(properties)
|
||||
.required(Collections.singletonList("wordLength")).build();
|
||||
Tools tools = Tools.builder()
|
||||
.type(Tools.Type.FUNCTION.getName())
|
||||
.function(ToolsFunction.builder().name("getOneWord").description("获取一个指定长度和语言类型的词语").parameters(parameters).build())
|
||||
.build();
|
||||
|
||||
ChatCompletion chatCompletion = ChatCompletion
|
||||
.builder()
|
||||
.messages(Collections.singletonList(message))
|
||||
.tools(Collections.singletonList(tools))
|
||||
.model(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
|
||||
.build();
|
||||
openAiStreamClient.streamChatCompletion(chatCompletion, eventSourceListener);
|
||||
|
||||
try {
|
||||
countDownLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
ToolCalls openAiReturnToolCalls = eventSourceListener.getToolCalls();
|
||||
WordParam wordParam = JSONUtil.toBean(openAiReturnToolCalls.getFunction().getArguments(), WordParam.class);
|
||||
String oneWord = getOneWord(wordParam);
|
||||
|
||||
|
||||
ToolCallFunction tcf = ToolCallFunction.builder().name("getOneWord").arguments(openAiReturnToolCalls.getFunction().getArguments()).build();
|
||||
ToolCalls tc = ToolCalls.builder().id(openAiReturnToolCalls.getId()).type(ToolCalls.Type.FUNCTION.getName()).function(tcf).build();
|
||||
//构造tool call
|
||||
Message message2 = Message.builder().role(Message.Role.ASSISTANT).content("方法参数").toolCalls(Collections.singletonList(tc)).build();
|
||||
String content
|
||||
= "{ " +
|
||||
"\"wordLength\": \"3\", " +
|
||||
"\"language\": \"zh\", " +
|
||||
"\"word\": \"" + oneWord + "\"," +
|
||||
"\"用途\": [\"直接吃\", \"做沙拉\", \"售卖\"]" +
|
||||
"}";
|
||||
Message message3 = Message.builder().toolCallId(openAiReturnToolCalls.getId()).role(Message.Role.TOOL).name("getOneWord").content(content).build();
|
||||
List<Message> messageList = Arrays.asList(message, message2, message3);
|
||||
ChatCompletion chatCompletionV2 = ChatCompletion
|
||||
.builder()
|
||||
.messages(messageList)
|
||||
.model(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
|
||||
.build();
|
||||
|
||||
|
||||
CountDownLatch countDownLatch1 = new CountDownLatch(1);
|
||||
openAiStreamClient.streamChatCompletion(chatCompletionV2, new ConsoleEventSourceListenerV3(countDownLatch));
|
||||
try {
|
||||
countDownLatch1.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
countDownLatch1.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
static class WordParam {
|
||||
private int wordLength;
|
||||
@Builder.Default
|
||||
private String language = "zh";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取一个词语(根据语言和字符长度查询)
|
||||
* @param wordParam
|
||||
* @return
|
||||
*/
|
||||
public String getOneWord(WordParam wordParam) {
|
||||
|
||||
List<String> zh = Arrays.asList("大香蕉", "哈密瓜", "苹果");
|
||||
List<String> en = Arrays.asList("apple", "banana", "cantaloupe");
|
||||
if (wordParam.getLanguage().equals("zh")) {
|
||||
for (String e : zh) {
|
||||
if (e.length() == wordParam.getWordLength()) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wordParam.getLanguage().equals("en")) {
|
||||
for (String e : en) {
|
||||
if (e.length() == wordParam.getWordLength()) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "西瓜";
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.ruoyi.common.chat.demo;
|
||||
|
||||
|
||||
import org.ruoyi.common.chat.openai.plugin.PluginAbstract;
|
||||
|
||||
public class WeatherPlugin extends PluginAbstract<WeatherReq, WeatherResp> {
|
||||
|
||||
public WeatherPlugin(Class<?> r) {
|
||||
super(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeatherResp func(WeatherReq args) {
|
||||
WeatherResp weatherResp = new WeatherResp();
|
||||
weatherResp.setTemp("25到28摄氏度");
|
||||
weatherResp.setLevel(3);
|
||||
return weatherResp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String content(WeatherResp weatherResp) {
|
||||
return "当前天气温度:" + weatherResp.getTemp() + ",风力等级:" + weatherResp.getLevel();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.ruoyi.common.chat.demo;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.chat.openai.plugin.PluginParam;
|
||||
|
||||
@Data
|
||||
public class WeatherReq extends PluginParam {
|
||||
/**
|
||||
* 城市
|
||||
*/
|
||||
private String location;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.ruoyi.common.chat.demo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WeatherResp {
|
||||
/**
|
||||
* 温度
|
||||
*/
|
||||
private String temp;
|
||||
/**
|
||||
* 风力等级
|
||||
*/
|
||||
private Integer level;
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
package org.ruoyi.common.chat.demo;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zhipu.oapi.ClientV4;
|
||||
import com.zhipu.oapi.Constants;
|
||||
import com.zhipu.oapi.service.v4.tools.*;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
|
||||
import com.zhipu.oapi.service.v4.model.*;
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class WebSearchToolsTest {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(WebSearchToolsTest.class);
|
||||
private static final String API_SECRET_KEY = "xx";
|
||||
|
||||
private static final ClientV4 client = new ClientV4.Builder(API_SECRET_KEY)
|
||||
.networkConfig(300, 100, 100, 100, TimeUnit.SECONDS)
|
||||
.connectionPool(new okhttp3.ConnectionPool(8, 1, TimeUnit.SECONDS))
|
||||
.build();
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
// 请自定义自己的业务id
|
||||
private static final String requestIdTemplate = "mycompany-%d";
|
||||
|
||||
|
||||
@Test
|
||||
public void test1() throws JsonProcessingException {
|
||||
|
||||
// json 转换 ArrayList<SearchChatMessage>
|
||||
String jsonString = "[\n" +
|
||||
" {\n" +
|
||||
" \"content\": \"今天武汉天气怎么样\",\n" +
|
||||
" \"role\": \"user\"\n" +
|
||||
" }\n" +
|
||||
" ]";
|
||||
|
||||
ArrayList<SearchChatMessage> messages = new ObjectMapper().readValue(jsonString, new TypeReference<ArrayList<SearchChatMessage>>() {
|
||||
});
|
||||
|
||||
|
||||
String requestId = String.format(requestIdTemplate, System.currentTimeMillis());
|
||||
WebSearchParamsRequest chatCompletionRequest = WebSearchParamsRequest.builder()
|
||||
.model("web-search-pro")
|
||||
.stream(Boolean.TRUE)
|
||||
.messages(messages)
|
||||
.requestId(requestId)
|
||||
.build();
|
||||
WebSearchApiResponse webSearchApiResponse = client.webSearchProStreamingInvoke(chatCompletionRequest);
|
||||
if (webSearchApiResponse.isSuccess()) {
|
||||
AtomicBoolean isFirst = new AtomicBoolean(true);
|
||||
List<ChoiceDelta> choices = new ArrayList<>();
|
||||
AtomicReference<WebSearchPro> lastAccumulator = new AtomicReference<>();
|
||||
|
||||
webSearchApiResponse.getFlowable().map(result -> result)
|
||||
.doOnNext(accumulator -> {
|
||||
{
|
||||
if (isFirst.getAndSet(false)) {
|
||||
logger.info("Response: ");
|
||||
}
|
||||
ChoiceDelta delta = accumulator.getChoices().get(0).getDelta();
|
||||
if (delta != null && delta.getToolCalls() != null) {
|
||||
logger.info("tool_calls: {}", mapper.writeValueAsString(delta.getToolCalls()));
|
||||
}
|
||||
choices.add(delta);
|
||||
lastAccumulator.set(accumulator);
|
||||
|
||||
}
|
||||
})
|
||||
.doOnComplete(() -> System.out.println("Stream completed."))
|
||||
.doOnError(throwable -> System.err.println("Error: " + throwable)) // Handle errors
|
||||
.blockingSubscribe();// Use blockingSubscribe instead of blockingGet()
|
||||
|
||||
WebSearchPro chatMessageAccumulator = lastAccumulator.get();
|
||||
|
||||
webSearchApiResponse.setFlowable(null);// 打印前置空
|
||||
webSearchApiResponse.setData(chatMessageAccumulator);
|
||||
}
|
||||
logger.info("model output: {}", mapper.writeValueAsString(webSearchApiResponse));
|
||||
client.getConfig().getHttpClient().dispatcher().executorService().shutdown();
|
||||
|
||||
client.getConfig().getHttpClient().connectionPool().evictAll();
|
||||
// List all active threads
|
||||
for (Thread t : Thread.getAllStackTraces().keySet()) {
|
||||
logger.info("Thread: " + t.getName() + " State: " + t.getState());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void test2() throws JsonProcessingException {
|
||||
|
||||
// json 转换 ArrayList<SearchChatMessage>
|
||||
String jsonString = "[\n" +
|
||||
" {\n" +
|
||||
" \"content\": \"今天天气怎么样\",\n" +
|
||||
" \"role\": \"user\"\n" +
|
||||
" }\n" +
|
||||
" ]";
|
||||
|
||||
ArrayList<SearchChatMessage> messages = new ObjectMapper().readValue(jsonString, new TypeReference<ArrayList<SearchChatMessage>>() {
|
||||
});
|
||||
|
||||
|
||||
String requestId = String.format(requestIdTemplate, System.currentTimeMillis());
|
||||
WebSearchParamsRequest chatCompletionRequest = WebSearchParamsRequest.builder()
|
||||
.model("web-search-pro")
|
||||
.stream(Boolean.FALSE)
|
||||
.messages(messages)
|
||||
.requestId(requestId)
|
||||
.build();
|
||||
WebSearchApiResponse webSearchApiResponse = client.invokeWebSearchPro(chatCompletionRequest);
|
||||
|
||||
logger.info("model output: {}", mapper.writeValueAsString(webSearchApiResponse));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testFunctionSSE() throws JsonProcessingException {
|
||||
List<ChatMessage> messages = new ArrayList<>();
|
||||
ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), "成都到北京要多久,天气如何");
|
||||
messages.add(chatMessage);
|
||||
String requestId = String.format(requestIdTemplate, System.currentTimeMillis());
|
||||
// 函数调用参数构建部分
|
||||
List<ChatTool> chatToolList = new ArrayList<>();
|
||||
ChatTool chatTool = new ChatTool();
|
||||
|
||||
chatTool.setType(ChatToolType.FUNCTION.value());
|
||||
ChatFunctionParameters chatFunctionParameters = new ChatFunctionParameters();
|
||||
chatFunctionParameters.setType("object");
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("location", new HashMap<String, Object>() {{
|
||||
put("type", "string");
|
||||
put("description", "城市,如:北京");
|
||||
}});
|
||||
properties.put("unit", new HashMap<String, Object>() {{
|
||||
put("type", "string");
|
||||
put("enum", new ArrayList<String>() {{
|
||||
add("celsius");
|
||||
add("fahrenheit");
|
||||
}});
|
||||
}});
|
||||
chatFunctionParameters.setProperties(properties);
|
||||
ChatFunction chatFunction = ChatFunction.builder()
|
||||
.name("get_weather")
|
||||
.description("Get the current weather of a location")
|
||||
.parameters(chatFunctionParameters)
|
||||
.build();
|
||||
chatTool.setFunction(chatFunction);
|
||||
chatToolList.add(chatTool);
|
||||
HashMap<String, Object> extraJson = new HashMap<>();
|
||||
extraJson.put("temperature", 0.5);
|
||||
extraJson.put("max_tokens", 50);
|
||||
|
||||
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
|
||||
.model(Constants.ModelChatGLM4)
|
||||
.stream(Boolean.TRUE)
|
||||
.messages(messages)
|
||||
.requestId(requestId)
|
||||
.tools(chatToolList)
|
||||
.toolChoice("auto")
|
||||
.extraJson(extraJson)
|
||||
.build();
|
||||
ModelApiResponse sseModelApiResp = client.invokeModelApi(chatCompletionRequest);
|
||||
if (sseModelApiResp.isSuccess()) {
|
||||
AtomicBoolean isFirst = new AtomicBoolean(true);
|
||||
List<Choice> choices = new ArrayList<>();
|
||||
ChatMessageAccumulator chatMessageAccumulator = mapStreamToAccumulator(sseModelApiResp.getFlowable())
|
||||
.doOnNext(accumulator -> {
|
||||
{
|
||||
if (isFirst.getAndSet(false)) {
|
||||
logger.info("Response: ");
|
||||
}
|
||||
if (accumulator.getDelta() != null && accumulator.getDelta().getTool_calls() != null) {
|
||||
String jsonString = mapper.writeValueAsString(accumulator.getDelta().getTool_calls());
|
||||
logger.info("tool_calls: {}", jsonString);
|
||||
}
|
||||
if (accumulator.getDelta() != null && accumulator.getDelta().getContent() != null) {
|
||||
logger.info(accumulator.getDelta().getContent());
|
||||
}
|
||||
choices.add(accumulator.getChoice());
|
||||
}
|
||||
})
|
||||
.doOnComplete(System.out::println)
|
||||
.lastElement()
|
||||
.blockingGet();
|
||||
|
||||
|
||||
ModelData data = new ModelData();
|
||||
data.setChoices(choices);
|
||||
data.setUsage(chatMessageAccumulator.getUsage());
|
||||
data.setId(chatMessageAccumulator.getId());
|
||||
data.setCreated(chatMessageAccumulator.getCreated());
|
||||
data.setRequestId(chatCompletionRequest.getRequestId());
|
||||
sseModelApiResp.setFlowable(null);// 打印前置空
|
||||
sseModelApiResp.setData(data);
|
||||
}
|
||||
logger.info("model output: {}", mapper.writeValueAsString(sseModelApiResp));
|
||||
}
|
||||
|
||||
public static Flowable<ChatMessageAccumulator> mapStreamToAccumulator(Flowable<ModelData> flowable) {
|
||||
return flowable.map(chunk -> {
|
||||
return new ChatMessageAccumulator(chunk.getChoices().get(0).getDelta(), null, chunk.getChoices().get(0), chunk.getUsage(), chunk.getCreated(), chunk.getId());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.ruoyi.common.chat.domain.request;
|
||||
|
||||
import org.ruoyi.common.chat.entity.chat.Message;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 描述:
|
||||
*
|
||||
* @author https:www.unfbx.com
|
||||
* @sine 2023-04-08
|
||||
*/
|
||||
@Data
|
||||
public class ChatRequest {
|
||||
|
||||
|
||||
private String frequency_penalty;
|
||||
|
||||
private String max_tokens;
|
||||
|
||||
@NotEmpty(message = "对话消息不能为空")
|
||||
List<Message> messages;
|
||||
|
||||
@NotEmpty(message = "传入的模型不能为空")
|
||||
private String model;
|
||||
|
||||
private String presence_penalty;
|
||||
|
||||
private String stream;
|
||||
|
||||
private double temperature;
|
||||
|
||||
private double top_p = 1;
|
||||
|
||||
/**
|
||||
* 知识库id
|
||||
*/
|
||||
private String kid;
|
||||
|
||||
private String userId;
|
||||
//
|
||||
|
||||
//
|
||||
// /**
|
||||
// * gpt的默认设置
|
||||
// */
|
||||
// private String systemMessage = "";
|
||||
//
|
||||
//
|
||||
//
|
||||
// private double temperature = 0.2;
|
||||
//
|
||||
// /**
|
||||
// * 上下文的条数
|
||||
// */
|
||||
// private Integer contentNumber = 10;
|
||||
//
|
||||
// /**
|
||||
// * 是否携带上下文
|
||||
// */
|
||||
// private Boolean usingContext = Boolean.TRUE;
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.ruoyi.common.chat.domain.request;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 描述:
|
||||
*
|
||||
* @author https:www.unfbx.com
|
||||
* @sine 2023-04-08
|
||||
*/
|
||||
@Data
|
||||
public class Dall3Request {
|
||||
|
||||
@NotEmpty(message = "传入的模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotEmpty(message = "提示词不能为空")
|
||||
private String prompt;
|
||||
|
||||
/** 图片大小 */
|
||||
@NotEmpty(message = "图片大小不能为空")
|
||||
private String size ;
|
||||
|
||||
/** 图片质量 */
|
||||
@NotEmpty(message = "图片质量不能为空")
|
||||
private String quality;
|
||||
|
||||
/** 图片风格 */
|
||||
@NotEmpty(message = "图片风格不能为空")
|
||||
private String style;
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.ruoyi.common.chat.entity.chat;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.chat.entity.chat.tool.ToolCalls;
|
||||
|
||||
@@ -20,6 +21,8 @@ import java.util.List;
|
||||
public class Message extends BaseMessage implements Serializable {
|
||||
|
||||
private Object content;
|
||||
@JsonProperty("reasoning_content")
|
||||
private String reasoningContent;
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.ruoyi.common.chat.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.config.LocalCache;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
|
||||
@@ -12,7 +11,6 @@ import org.ruoyi.common.chat.listener.WebSocketEventListener;
|
||||
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
|
||||
import org.ruoyi.common.chat.utils.WebSocketUtils;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.socket.*;
|
||||
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
|
||||
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
package org.ruoyi.common.chat.localModels;
|
||||
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.ruoyi.common.chat.entity.models.LocalModelsSearchRequest;
|
||||
import org.ruoyi.common.chat.entity.models.LocalModelsSearchResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.jackson.JacksonConverterFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class LocalModelsofitClient {
|
||||
private static final String BASE_URL = "http://127.0.0.1:5000"; // Flask 服务的 URL
|
||||
private static Retrofit retrofit = null;
|
||||
|
||||
// 获取 Retrofit 实例
|
||||
public static Retrofit getRetrofitInstance() {
|
||||
if (retrofit == null) {
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.build();
|
||||
|
||||
retrofit = new Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(client)
|
||||
.addConverterFactory(JacksonConverterFactory.create()) // 使用 Jackson 处理 JSON 转换
|
||||
.build();
|
||||
}
|
||||
return retrofit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 Flask 服务发送文本向量化请求
|
||||
*
|
||||
* @param queries 查询文本列表
|
||||
* @param modelName 模型名称
|
||||
* @param delimiter 文本分隔符
|
||||
* @param topK 返回的结果数
|
||||
* @param blockSize 文本块大小
|
||||
* @param overlapChars 重叠字符数
|
||||
* @return 返回计算得到的 Top K 嵌入向量列表
|
||||
*/
|
||||
|
||||
public static List<List<Double>> getTopKEmbeddings(
|
||||
List<String> queries,
|
||||
String modelName,
|
||||
String delimiter,
|
||||
int topK,
|
||||
int blockSize,
|
||||
int overlapChars) {
|
||||
|
||||
modelName = (!StringUtils.isEmpty(modelName)) ? modelName : "msmarco-distilbert-base-tas-b"; // 默认模型名称
|
||||
delimiter = (!StringUtils.isEmpty(delimiter) ) ? delimiter : "."; // 默认分隔符
|
||||
topK = (topK > 0) ? topK : 3; // 默认返回 3 个结果
|
||||
blockSize = (blockSize > 0) ? blockSize : 500; // 默认文本块大小为 500
|
||||
overlapChars = (overlapChars > 0) ? overlapChars : 50; // 默认重叠字符数为 50
|
||||
|
||||
// 创建 Retrofit 实例
|
||||
Retrofit retrofit = getRetrofitInstance();
|
||||
|
||||
// 创建 SearchService 接口
|
||||
SearchService service = retrofit.create(SearchService.class);
|
||||
|
||||
// 创建请求对象 LocalModelsSearchRequest
|
||||
LocalModelsSearchRequest request = new LocalModelsSearchRequest(
|
||||
queries, // 查询文本列表
|
||||
modelName, // 模型名称
|
||||
delimiter, // 文本分隔符
|
||||
topK, // 返回的结果数
|
||||
blockSize, // 文本块大小
|
||||
overlapChars // 重叠字符数
|
||||
);
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1); // 创建一个 CountDownLatch
|
||||
final List<List<Double>>[] topKEmbeddings = new List[]{null}; // 使用数组来存储结果(因为 Java 不支持直接修改 List)
|
||||
|
||||
// 发起异步请求
|
||||
service.vectorize(request).enqueue(new Callback<LocalModelsSearchResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<LocalModelsSearchResponse> call, Response<LocalModelsSearchResponse> response) {
|
||||
if (response.isSuccessful()) {
|
||||
LocalModelsSearchResponse searchResponse = response.body();
|
||||
if (searchResponse != null) {
|
||||
topKEmbeddings[0] = searchResponse.getTopKEmbeddings().get(0); // 获取结果
|
||||
log.info("Successfully retrieved embeddings");
|
||||
} else {
|
||||
log.error("Response body is null");
|
||||
}
|
||||
} else {
|
||||
log.error("Request failed. HTTP error code: " + response.code());
|
||||
}
|
||||
latch.countDown(); // 请求完成,减少计数
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<LocalModelsSearchResponse> call, Throwable t) {
|
||||
t.printStackTrace();
|
||||
log.error("Request failed: ", t);
|
||||
latch.countDown(); // 请求失败,减少计数
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await(); // 等待请求完成
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return topKEmbeddings[0]; // 返回结果
|
||||
}
|
||||
|
||||
// public static void main(String[] args) {
|
||||
// // 示例调用
|
||||
// List<String> queries = Arrays.asList("What is artificial intelligence?", "AI is transforming industries.");
|
||||
// String modelName = "msmarco-distilbert-base-tas-b";
|
||||
// String delimiter = ".";
|
||||
// int topK = 3;
|
||||
// int blockSize = 500;
|
||||
// int overlapChars = 50;
|
||||
//
|
||||
// List<List<Double>> topKEmbeddings = getTopKEmbeddings(queries, modelName, delimiter, topK, blockSize, overlapChars);
|
||||
//
|
||||
// // 打印结果
|
||||
// if (topKEmbeddings != null) {
|
||||
// System.out.println("Top K embeddings: ");
|
||||
// for (List<Double> embedding : topKEmbeddings) {
|
||||
// System.out.println(embedding);
|
||||
// }
|
||||
// } else {
|
||||
// System.out.println("No embeddings returned.");
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// public static void main(String[] args) {
|
||||
// // 创建 Retrofit 实例
|
||||
// Retrofit retrofit = LocalModelsofitClient.getRetrofitInstance();
|
||||
//
|
||||
// // 创建 SearchService 接口
|
||||
// SearchService service = retrofit.create(SearchService.class);
|
||||
//
|
||||
// // 创建请求对象 LocalModelsSearchRequest
|
||||
// LocalModelsSearchRequest request = new LocalModelsSearchRequest(
|
||||
// Arrays.asList("What is artificial intelligence?", "AI is transforming industries."), // 查询文本列表
|
||||
// "msmarco-distilbert-base-tas-b", // 模型名称
|
||||
// ".", // 分隔符
|
||||
// 3, // 返回的结果数
|
||||
// 500, // 文本块大小
|
||||
// 50 // 重叠字符数
|
||||
// );
|
||||
//
|
||||
// // 发起请求
|
||||
// service.vectorize(request).enqueue(new Callback<LocalModelsSearchResponse>() {
|
||||
// @Override
|
||||
// public void onResponse(Call<LocalModelsSearchResponse> call, Response<LocalModelsSearchResponse> response) {
|
||||
// if (response.isSuccessful()) {
|
||||
// LocalModelsSearchResponse searchResponse = response.body();
|
||||
// System.out.println("Response Body: " + response.body()); // Print the whole response body for debugging
|
||||
//
|
||||
// if (searchResponse != null) {
|
||||
// // If the response is not null, process it.
|
||||
// // Example: Extract the embeddings and print them
|
||||
// List<List<List<Double>>> topKEmbeddings = searchResponse.getTopKEmbeddings();
|
||||
// if (topKEmbeddings != null) {
|
||||
// // Print the Top K embeddings
|
||||
//
|
||||
// } else {
|
||||
// System.err.println("Top K embeddings are null");
|
||||
// }
|
||||
//
|
||||
// // If there is more information you want to process, handle it here
|
||||
//
|
||||
// } else {
|
||||
// System.err.println("Response body is null");
|
||||
// }
|
||||
// } else {
|
||||
// System.err.println("Request failed. HTTP error code: " + response.code());
|
||||
// log.error("Failed to retrieve data. HTTP error code: " + response.code());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onFailure(Call<LocalModelsSearchResponse> call, Throwable t) {
|
||||
// // 请求失败,打印错误
|
||||
// t.printStackTrace();
|
||||
// log.error("Request failed: ", t);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.ruoyi.common.chat.localModels;
|
||||
|
||||
|
||||
|
||||
import org.ruoyi.common.chat.entity.models.LocalModelsSearchRequest;
|
||||
import org.ruoyi.common.chat.entity.models.LocalModelsSearchResponse;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.POST;
|
||||
/**
|
||||
* @program: RUOYIAI
|
||||
* @ClassName SearchService
|
||||
* @description: 请求模型
|
||||
* @author: hejh
|
||||
* @create: 2025-03-15 17:27
|
||||
* @Version 1.0
|
||||
**/
|
||||
|
||||
|
||||
public interface SearchService {
|
||||
@POST("/vectorize") // 与 Flask 服务中的路由匹配
|
||||
Call<LocalModelsSearchResponse> vectorize(@Body LocalModelsSearchRequest request);
|
||||
}
|
||||
|
||||
|
||||
@@ -466,8 +466,8 @@ public class OpenAiStreamClient {
|
||||
* @since 1.1.3
|
||||
*/
|
||||
public ResponseBody textToSpeech(TextToSpeech textToSpeech){
|
||||
Call<ResponseBody> responseBody = this.openAiApi.textToSpeech(textToSpeech);
|
||||
try {
|
||||
Call<ResponseBody> responseBody = this.openAiApi.textToSpeech(textToSpeech);
|
||||
return responseBody.execute().body();
|
||||
} catch (IOException e) {
|
||||
throw new BaseException("文本转语音(同步)失败: "+e.getMessage());
|
||||
@@ -593,11 +593,6 @@ public class OpenAiStreamClient {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package org.ruoyi.common.chat.plugin;
|
||||
|
||||
import org.ruoyi.common.chat.openai.plugin.PluginAbstract;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CmdPlugin extends PluginAbstract<CmdReq, CmdResp> {
|
||||
|
||||
public CmdPlugin(Class<?> r) {
|
||||
super(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CmdResp func(CmdReq args) {
|
||||
try {
|
||||
if("计算器".equals(args.getCmd())){
|
||||
Runtime.getRuntime().exec("calc");
|
||||
}else if("记事本".equals(args.getCmd())){
|
||||
Runtime.getRuntime().exec("notepad");
|
||||
}else if("命令行".equals(args.getCmd())){
|
||||
String [] cmd={"cmd","/C","start copy exel exe2"};
|
||||
Runtime.getRuntime().exec(cmd);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("指令执行失败");
|
||||
}
|
||||
CmdResp resp = new CmdResp();
|
||||
resp.setResult(args.getCmd()+"指令执行成功!");
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String content(CmdResp resp) {
|
||||
return resp.getResult();
|
||||
}
|
||||
}
|
||||
@@ -2,31 +2,49 @@ package org.ruoyi.common.chat.request;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.chat.entity.chat.Content;
|
||||
import org.ruoyi.common.chat.entity.chat.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 描述:
|
||||
* 描述:对话请求对象
|
||||
*
|
||||
* @author https:www.unfbx.com
|
||||
* @author ageerle
|
||||
* @sine 2023-04-08
|
||||
*/
|
||||
@Data
|
||||
public class ChatRequest {
|
||||
|
||||
@NotEmpty(message = "传入的模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotEmpty(message = "对话消息不能为空")
|
||||
List<Message> messages;
|
||||
|
||||
List<Content> imageContent;
|
||||
@NotEmpty(message = "传入的模型不能为空")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
private String userId;
|
||||
/**
|
||||
* 系统提示词
|
||||
*/
|
||||
private String sysPrompt;
|
||||
|
||||
/**
|
||||
* 是否开启流式对话
|
||||
*/
|
||||
private Boolean stream = Boolean.TRUE;
|
||||
|
||||
/**
|
||||
* 是否开启联网搜索(0关闭 1开启)
|
||||
*/
|
||||
private Boolean search = Boolean.FALSE;
|
||||
|
||||
/**
|
||||
* 是否开启mcp
|
||||
*/
|
||||
private Boolean isMcp = Boolean.FALSE;
|
||||
|
||||
/**
|
||||
* 知识库id
|
||||
@@ -34,13 +52,14 @@ public class ChatRequest {
|
||||
private String kid;
|
||||
|
||||
/**
|
||||
* gpt的默认设置
|
||||
* 用户id
|
||||
*/
|
||||
private String systemMessage = "";
|
||||
private Long userId;
|
||||
|
||||
private double top_p = 1;
|
||||
|
||||
private double temperature = 0.2;
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 上下文的条数
|
||||
@@ -52,4 +71,5 @@ public class ChatRequest {
|
||||
*/
|
||||
private Boolean usingContext = Boolean.TRUE;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ public class ConsoleEventSourceListener extends EventSourceListener {
|
||||
log.info("OpenAI返回数据:{}", data);
|
||||
if ("[DONE]".equals(data)) {
|
||||
log.info("OpenAI返回数据结束了");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import okhttp3.ResponseBody;
|
||||
import okhttp3.sse.EventSource;
|
||||
import okhttp3.sse.EventSourceListener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.ruoyi.common.chat.constant.OpenAIConst;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
|
||||
import org.ruoyi.common.chat.entity.chat.FunctionCall;
|
||||
|
||||
@@ -47,25 +47,11 @@
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- hutool工具模块 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-http</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-extra</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-json</artifactId>
|
||||
<scope>provided</scope>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -73,18 +59,6 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-cp</artifactId>
|
||||
<version>${weixin-java-miniapp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-cp</artifactId>
|
||||
<version>${weixin-java-cp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 自动生成YML配置关联JSON文件 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -108,6 +82,11 @@
|
||||
<artifactId>ip2region</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-cp</artifactId>
|
||||
<version>${weixin-java-cp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
@@ -130,16 +109,19 @@
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>${okhttp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp-sse</artifactId>
|
||||
<version>${okhttp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>logging-interceptor</artifactId>
|
||||
<version>${okhttp.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.ruoyi.common.core.constant;
|
||||
|
||||
import cn.hutool.core.lang.RegexPool;
|
||||
|
||||
/**
|
||||
* 常用正则表达式字符串
|
||||
* <p>
|
||||
* 常用正则表达式集合,更多正则见: https://any86.github.io/any-rule/
|
||||
*
|
||||
* @author Feng
|
||||
*/
|
||||
public interface RegexConstants extends RegexPool {
|
||||
|
||||
/**
|
||||
* 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
|
||||
*/
|
||||
String DICTIONARY_TYPE = "^[a-z][a-z0-9_]*$";
|
||||
|
||||
/**
|
||||
* 权限标识必须符合以下格式:
|
||||
* 1. 标准格式:xxx:yyy:zzz
|
||||
* - 第一部分(xxx):只能包含字母、数字和下划线(_),不能使用 `*`
|
||||
* - 第二部分(yyy):可以包含字母、数字、下划线(_)和 `*`
|
||||
* - 第三部分(zzz):可以包含字母、数字、下划线(_)和 `*`
|
||||
* 2. 允许空字符串(""),表示没有权限标识
|
||||
*/
|
||||
String PERMISSION_STRING = "^$|^[a-zA-Z0-9_]+:[a-zA-Z0-9_*]+:[a-zA-Z0-9_*]+$";
|
||||
|
||||
/**
|
||||
* 身份证号码(后6位)
|
||||
*/
|
||||
String ID_CARD_LAST_6 = "^(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
|
||||
|
||||
/**
|
||||
* QQ号码
|
||||
*/
|
||||
String QQ_NUMBER = "^[1-9][0-9]\\d{4,9}$";
|
||||
|
||||
/**
|
||||
* 邮政编码
|
||||
*/
|
||||
String POSTAL_CODE = "^[1-9]\\d{5}$";
|
||||
|
||||
/**
|
||||
* 注册账号
|
||||
*/
|
||||
String ACCOUNT = "^[a-zA-Z][a-zA-Z0-9_]{4,15}$";
|
||||
|
||||
/**
|
||||
* 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
|
||||
*/
|
||||
String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
|
||||
|
||||
/**
|
||||
* 通用状态(0表示正常,1表示停用)
|
||||
*/
|
||||
String STATUS = "^[01]$";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.ruoyi.common.core.constant;
|
||||
|
||||
/**
|
||||
* 系统常量信息
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public interface SystemConstants {
|
||||
|
||||
/**
|
||||
* 正常状态
|
||||
*/
|
||||
String NORMAL = "0";
|
||||
|
||||
/**
|
||||
* 异常状态
|
||||
*/
|
||||
String DISABLE = "1";
|
||||
|
||||
/**
|
||||
* 是否为系统默认(是)
|
||||
*/
|
||||
String YES = "Y";
|
||||
|
||||
/**
|
||||
* 是否为系统默认(否)
|
||||
*/
|
||||
String NO = "N";
|
||||
|
||||
/**
|
||||
* 是否菜单外链(是)
|
||||
*/
|
||||
String YES_FRAME = "0";
|
||||
|
||||
/**
|
||||
* 是否菜单外链(否)
|
||||
*/
|
||||
String NO_FRAME = "1";
|
||||
|
||||
/**
|
||||
* 菜单类型(目录)
|
||||
*/
|
||||
String TYPE_DIR = "M";
|
||||
|
||||
/**
|
||||
* 菜单类型(菜单)
|
||||
*/
|
||||
String TYPE_MENU = "C";
|
||||
|
||||
/**
|
||||
* 菜单类型(按钮)
|
||||
*/
|
||||
String TYPE_BUTTON = "F";
|
||||
|
||||
/**
|
||||
* Layout组件标识
|
||||
*/
|
||||
String LAYOUT = "Layout";
|
||||
|
||||
/**
|
||||
* ParentView组件标识
|
||||
*/
|
||||
String PARENT_VIEW = "ParentView";
|
||||
|
||||
/**
|
||||
* InnerLink组件标识
|
||||
*/
|
||||
String INNER_LINK = "InnerLink";
|
||||
|
||||
/**
|
||||
* 超级管理员ID
|
||||
*/
|
||||
Long SUPER_ADMIN_ID = 1L;
|
||||
|
||||
/**
|
||||
* 根部门祖级列表
|
||||
*/
|
||||
String ROOT_DEPT_ANCESTORS = "0";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.ruoyi.common.core.factory;
|
||||
|
||||
import cn.hutool.core.lang.PatternPool;
|
||||
import org.ruoyi.common.core.constant.RegexConstants;
|
||||
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 正则表达式模式池工厂
|
||||
* <p>初始化的时候将正则表达式加入缓存池当中</p>
|
||||
* <p>提高正则表达式的性能,避免重复编译相同的正则表达式</p>
|
||||
*
|
||||
* @author 21001
|
||||
*/
|
||||
public class RegexPatternPoolFactory extends PatternPool {
|
||||
|
||||
/**
|
||||
* 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
|
||||
*/
|
||||
public static final Pattern DICTIONARY_TYPE = get(RegexConstants.DICTIONARY_TYPE);
|
||||
|
||||
/**
|
||||
* 身份证号码(后6位)
|
||||
*/
|
||||
public static final Pattern ID_CARD_LAST_6 = get(RegexConstants.ID_CARD_LAST_6);
|
||||
|
||||
/**
|
||||
* QQ号码
|
||||
*/
|
||||
public static final Pattern QQ_NUMBER = get(RegexConstants.QQ_NUMBER);
|
||||
|
||||
/**
|
||||
* 邮政编码
|
||||
*/
|
||||
public static final Pattern POSTAL_CODE = get(RegexConstants.POSTAL_CODE);
|
||||
|
||||
/**
|
||||
* 注册账号
|
||||
*/
|
||||
public static final Pattern ACCOUNT = get(RegexConstants.ACCOUNT);
|
||||
|
||||
/**
|
||||
* 密码:包含至少8个字符,包括大写字母、小写字母、数字和特殊字符
|
||||
*/
|
||||
public static final Pattern PASSWORD = get(RegexConstants.PASSWORD);
|
||||
|
||||
/**
|
||||
* 通用状态(0表示正常,1表示停用)
|
||||
*/
|
||||
public static final Pattern STATUS = get(RegexConstants.STATUS);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.ruoyi.common.core.factory;
|
||||
|
||||
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.support.DefaultPropertySourceFactory;
|
||||
import org.springframework.core.io.support.EncodedResource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* yml 配置源工厂
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class YmlPropertySourceFactory extends DefaultPropertySourceFactory {
|
||||
|
||||
@Override
|
||||
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
|
||||
String sourceName = resource.getResource().getFilename();
|
||||
if (StringUtils.isNotBlank(sourceName) && StringUtils.endsWithAny(sourceName, ".yml", ".yaml")) {
|
||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||
factory.setResources(resource.getResource());
|
||||
factory.afterPropertiesSet();
|
||||
return new PropertiesPropertySource(sourceName, factory.getObject());
|
||||
}
|
||||
return super.createPropertySource(name, resource);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.ruoyi.common.core.utils;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 对象工具类
|
||||
*
|
||||
* @author 秋辞未寒
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ObjectUtils extends ObjectUtil {
|
||||
|
||||
/**
|
||||
* 如果对象不为空,则获取对象中的某个字段 ObjectUtils.notNullGetter(user, User::getName);
|
||||
*
|
||||
* @param obj 对象
|
||||
* @param func 获取方法
|
||||
* @return 对象字段
|
||||
*/
|
||||
public static <T, E> E notNullGetter(T obj, Function<T, E> func) {
|
||||
if (isNotNull(obj) && isNotNull(func)) {
|
||||
return func.apply(obj);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果对象不为空,则获取对象中的某个字段,否则返回默认值
|
||||
*
|
||||
* @param obj 对象
|
||||
* @param func 获取方法
|
||||
* @param defaultValue 默认值
|
||||
* @return 对象字段
|
||||
*/
|
||||
public static <T, E> E notNullGetter(T obj, Function<T, E> func, E defaultValue) {
|
||||
if (isNotNull(obj) && isNotNull(func)) {
|
||||
return func.apply(obj);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果值不为空,则返回值,否则返回默认值
|
||||
*
|
||||
* @param obj 对象
|
||||
* @param defaultValue 默认值
|
||||
* @return 对象字段
|
||||
*/
|
||||
public static <T> T notNull(T obj, T defaultValue) {
|
||||
if (isNotNull(obj)) {
|
||||
return obj;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.ruoyi.common.core.utils;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import org.springframework.aop.framework.AopContext;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.boot.autoconfigure.thread.Threading;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@@ -48,7 +49,7 @@ public final class SpringUtils extends SpringUtil {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getAopProxy(T invoker) {
|
||||
return (T) AopContext.currentProxy();
|
||||
return (T) getBean(invoker.getClass());
|
||||
}
|
||||
|
||||
|
||||
@@ -59,4 +60,8 @@ public final class SpringUtils extends SpringUtil {
|
||||
return getApplicationContext();
|
||||
}
|
||||
|
||||
public static boolean isVirtual() {
|
||||
return Threading.VIRTUAL.isActive(getBean(Environment.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,11 +23,6 @@
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
@@ -38,6 +33,23 @@
|
||||
<artifactId>hutool-crypto</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<optional>true</optional>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<packaging>pom</packaging>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>ruoyi-common-live</artifactId>
|
||||
|
||||
<description>
|
||||
弹幕监听
|
||||
AI直播
|
||||
</description>
|
||||
|
||||
<modules>
|
||||
@@ -31,7 +33,7 @@
|
||||
|
||||
<brotli4j.version>1.13.0</brotli4j.version>
|
||||
<jackson-databind.version>2.16.0</jackson-databind.version>
|
||||
<hutool-all.version>5.8.24</hutool-all.version>
|
||||
<hutool-all.version>5.8.35</hutool-all.version>
|
||||
<netty-all.version>4.1.104.Final</netty-all.version>
|
||||
<logback-classic.version>1.4.12</logback-classic.version>
|
||||
<lombok.version>1.18.30</lombok.version>
|
||||
|
||||
@@ -7,8 +7,7 @@ import org.ruoyi.common.core.service.ConfigService;
|
||||
import org.ruoyi.common.mail.utils.MailAccount;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
|
||||
/**
|
||||
* JavaMail 配置
|
||||
@@ -22,10 +21,9 @@ import org.springframework.scheduling.annotation.Scheduled;
|
||||
public class MailConfig {
|
||||
|
||||
private final ConfigService configService;
|
||||
private MailAccount account; // 缓存MailAccount实例
|
||||
private MailAccount account;
|
||||
|
||||
@Bean
|
||||
@Scope("singleton")
|
||||
public MailAccount mailAccount() {
|
||||
if (account == null) {
|
||||
account = new MailAccount();
|
||||
@@ -34,7 +32,6 @@ public class MailConfig {
|
||||
return account;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 10000) // 每10秒检查一次
|
||||
public void updateMailAccount() {
|
||||
account.setHost(getKey("host"));
|
||||
account.setPort(NumberUtils.toInt(getKey("port"), 465));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.ruoyi.common.mail.config.properties;
|
||||
package org.ruoyi.common.mail.properties;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -30,12 +29,17 @@
|
||||
<!-- dynamic-datasource 多数据源-->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- sql性能分析插件 -->
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.baomidou.dynamic.datasource.processor.jakarta;
|
||||
|
||||
import com.baomidou.dynamic.datasource.processor.DsProcessor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
/**
|
||||
* @author TaoYu
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public class DsJakartaHeaderProcessor extends DsProcessor {
|
||||
|
||||
/**
|
||||
* header prefix
|
||||
*/
|
||||
private static final String HEADER_PREFIX = "#header";
|
||||
|
||||
@Override
|
||||
public boolean matches(String key) {
|
||||
return key.startsWith(HEADER_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String doDetermineDatasource(MethodInvocation invocation, String key) {
|
||||
HttpServletRequest request = (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
return request.getHeader(key.substring(8));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.baomidou.dynamic.datasource.processor.jakarta;
|
||||
|
||||
import com.baomidou.dynamic.datasource.processor.DsProcessor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
|
||||
/**
|
||||
* @author TaoYu
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public class DsJakartaSessionProcessor extends DsProcessor {
|
||||
|
||||
/**
|
||||
* session开头
|
||||
*/
|
||||
private static final String SESSION_PREFIX = "#session";
|
||||
|
||||
@Override
|
||||
public boolean matches(String key) {
|
||||
return key.startsWith(SESSION_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String doDetermineDatasource(MethodInvocation invocation, String key) {
|
||||
HttpServletRequest request = (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
return request.getSession().getAttribute(key.substring(9)).toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.ruoyi.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限注解,用于标记数据权限的占位符关键字和替换值
|
||||
* <p>
|
||||
* 一个注解只能对应一个模板
|
||||
* </p>
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface DataColumn {
|
||||
|
||||
/**
|
||||
* 数据权限模板的占位符关键字,默认为 "deptName"
|
||||
*
|
||||
* @return 占位符关键字数组
|
||||
*/
|
||||
String[] key() default "deptName";
|
||||
|
||||
/**
|
||||
* 数据权限模板的占位符替换值,默认为 "dept_id"
|
||||
*
|
||||
* @return 占位符替换值数组
|
||||
*/
|
||||
String[] value() default "dept_id";
|
||||
|
||||
/**
|
||||
* 权限标识符 用于通过菜单权限标识符来获取数据权限
|
||||
* 拥有此标识符的角色 将不会拼接此角色的数据过滤sql
|
||||
*
|
||||
* @return 权限标识符
|
||||
*/
|
||||
String permission() default "";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.ruoyi.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限组注解,用于标记数据权限配置数组
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface DataPermission {
|
||||
|
||||
/**
|
||||
* 数据权限配置数组,用于指定数据权限的占位符关键字和替换值
|
||||
*
|
||||
* @return 数据权限配置数组
|
||||
*/
|
||||
DataColumn[] value();
|
||||
|
||||
/**
|
||||
* 权限拼接标识符(用于指定连接语句的sql符号)
|
||||
* 如不填 默认 select 用 OR 其他语句用 AND
|
||||
* 内容 OR 或者 AND
|
||||
*/
|
||||
String joinStr() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.ruoyi.aspect;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.AfterThrowing;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.ruoyi.annotation.DataPermission;
|
||||
import org.ruoyi.helper.DataPermissionHelper;
|
||||
|
||||
/**
|
||||
* 数据权限处理
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
public class DataPermissionAspect {
|
||||
|
||||
/**
|
||||
* 处理请求前执行
|
||||
*/
|
||||
@Before(value = "@annotation(dataPermission)")
|
||||
public void doBefore(JoinPoint joinPoint, DataPermission dataPermission) {
|
||||
DataPermissionHelper.setPermission(dataPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理完请求后执行
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@AfterReturning(pointcut = "@annotation(dataPermission)")
|
||||
public void doAfterReturning(JoinPoint joinPoint, DataPermission dataPermission) {
|
||||
DataPermissionHelper.removePermission();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截异常操作
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param e 异常
|
||||
*/
|
||||
@AfterThrowing(value = "@annotation(dataPermission)", throwing = "e")
|
||||
public void doAfterThrowing(JoinPoint joinPoint, DataPermission dataPermission, Exception e) {
|
||||
DataPermissionHelper.removePermission();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package org.ruoyi.common.mybatis.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限
|
||||
*
|
||||
* 一个注解只能对应一个模板
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface DataColumn {
|
||||
|
||||
/**
|
||||
* 占位符关键字
|
||||
*/
|
||||
String[] key() default "deptName";
|
||||
|
||||
/**
|
||||
* 占位符替换值
|
||||
*/
|
||||
String[] value() default "dept_id";
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package org.ruoyi.common.mybatis.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限组
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface DataPermission {
|
||||
|
||||
DataColumn[] value();
|
||||
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package org.ruoyi.common.mybatis.core.mapper;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.Db;
|
||||
import org.apache.ibatis.logging.Log;
|
||||
import org.apache.ibatis.logging.LogFactory;
|
||||
import org.ruoyi.common.core.utils.MapstructUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 自定义 Mapper 接口, 实现 自定义扩展
|
||||
*
|
||||
* @param <T> table 泛型
|
||||
* @param <V> vo 泛型
|
||||
* @author Lion Li
|
||||
* @since 2021-05-13
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
|
||||
Log log = LogFactory.getLog(BaseMapperPlus.class);
|
||||
|
||||
default Class<V> currentVoClass() {
|
||||
return (Class<V>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseMapperPlus.class, 1);
|
||||
}
|
||||
|
||||
default Class<T> currentModelClass() {
|
||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseMapperPlus.class, 0);
|
||||
}
|
||||
|
||||
default List<T> selectList() {
|
||||
return this.selectList(new QueryWrapper<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入
|
||||
*/
|
||||
default boolean insertBatch(Collection<T> entityList) {
|
||||
return Db.saveBatch(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新
|
||||
*/
|
||||
default boolean updateBatchById(Collection<T> entityList) {
|
||||
return Db.updateBatchById(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入或更新
|
||||
*/
|
||||
default boolean insertOrUpdateBatch(Collection<T> entityList) {
|
||||
return Db.saveOrUpdateBatch(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入(包含限制条数)
|
||||
*/
|
||||
default boolean insertBatch(Collection<T> entityList, int batchSize) {
|
||||
return Db.saveBatch(entityList, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新(包含限制条数)
|
||||
*/
|
||||
default boolean updateBatchById(Collection<T> entityList, int batchSize) {
|
||||
return Db.updateBatchById(entityList, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入或更新(包含限制条数)
|
||||
*/
|
||||
default boolean insertOrUpdateBatch(Collection<T> entityList, int batchSize) {
|
||||
return Db.saveOrUpdateBatch(entityList, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入或更新(包含限制条数)
|
||||
*/
|
||||
default boolean insertOrUpdate(T entity) {
|
||||
return Db.saveOrUpdate(entity);
|
||||
}
|
||||
|
||||
default V selectVoById(Serializable id) {
|
||||
return selectVoById(id, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 查询
|
||||
*/
|
||||
default <C> C selectVoById(Serializable id, Class<C> voClass) {
|
||||
T obj = this.selectById(id);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return null;
|
||||
}
|
||||
return MapstructUtils.convert(obj, voClass);
|
||||
}
|
||||
|
||||
default List<V> selectVoBatchIds(Collection<? extends Serializable> idList) {
|
||||
return selectVoBatchIds(idList, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询(根据ID 批量查询)
|
||||
*/
|
||||
default <C> List<C> selectVoBatchIds(Collection<? extends Serializable> idList, Class<C> voClass) {
|
||||
List<T> list = this.selectBatchIds(idList);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return MapstructUtils.convert(list, voClass);
|
||||
}
|
||||
|
||||
default List<V> selectVoByMap(Map<String, Object> map) {
|
||||
return selectVoByMap(map, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询(根据 columnMap 条件)
|
||||
*/
|
||||
default <C> List<C> selectVoByMap(Map<String, Object> map, Class<C> voClass) {
|
||||
List<T> list = this.selectByMap(map);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return MapstructUtils.convert(list, voClass);
|
||||
}
|
||||
|
||||
default V selectVoOne(Wrapper<T> wrapper) {
|
||||
return selectVoOne(wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 entity 条件,查询一条记录
|
||||
*/
|
||||
default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass) {
|
||||
T obj = this.selectOne(wrapper);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return null;
|
||||
}
|
||||
return MapstructUtils.convert(obj, voClass);
|
||||
}
|
||||
|
||||
default List<V> selectVoList() {
|
||||
return selectVoList(new QueryWrapper<>(), this.currentVoClass());
|
||||
}
|
||||
|
||||
default List<V> selectVoList(Wrapper<T> wrapper) {
|
||||
return selectVoList(wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 entity 条件,查询全部记录
|
||||
*/
|
||||
default <C> List<C> selectVoList(Wrapper<T> wrapper, Class<C> voClass) {
|
||||
List<T> list = this.selectList(wrapper);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return MapstructUtils.convert(list, voClass);
|
||||
}
|
||||
|
||||
default <P extends IPage<V>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper) {
|
||||
return selectVoPage(page, wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询VO
|
||||
*/
|
||||
default <C, P extends IPage<C>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper, Class<C> voClass) {
|
||||
IPage<T> pageData = this.selectPage(page, wrapper);
|
||||
IPage<C> voPage = new Page<>(pageData.getCurrent(), pageData.getSize(), pageData.getTotal());
|
||||
if (CollUtil.isEmpty(pageData.getRecords())) {
|
||||
return (P) voPage;
|
||||
}
|
||||
voPage.setRecords(MapstructUtils.convert(pageData.getRecords(), voClass));
|
||||
return (P) voPage;
|
||||
}
|
||||
|
||||
default <C> List<C> selectObjs(Wrapper<T> wrapper, Function<? super Object, C> mapper) {
|
||||
return this.selectObjs(wrapper).stream().filter(Objects::nonNull).map(mapper).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package org.ruoyi.common.mybatis.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.helper.DataPermissionHelper;
|
||||
|
||||
/**
|
||||
* 数据权限类型
|
||||
* <p>
|
||||
* 语法支持 spel 模板表达式
|
||||
* <p>
|
||||
* 内置数据 user 当前用户 内容参考 LoginUser
|
||||
* 如需扩展数据 可使用 {@link DataPermissionHelper} 操作
|
||||
* 内置服务 sdss 系统数据权限服务 内容参考 SysDataScopeService
|
||||
* 如需扩展更多自定义服务 可以参考 sdss 自行编写
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DataScopeType {
|
||||
|
||||
/**
|
||||
* 全部数据权限
|
||||
*/
|
||||
ALL("1", "", ""),
|
||||
|
||||
/**
|
||||
* 自定数据权限
|
||||
*/
|
||||
CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", ""),
|
||||
|
||||
/**
|
||||
* 部门数据权限
|
||||
*/
|
||||
DEPT("3", " #{#deptName} = #{#user.deptId} ", ""),
|
||||
|
||||
/**
|
||||
* 部门及以下数据权限
|
||||
*/
|
||||
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", ""),
|
||||
|
||||
/**
|
||||
* 仅本人数据权限
|
||||
*/
|
||||
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 ");
|
||||
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
* 语法 采用 spel 模板表达式
|
||||
*/
|
||||
private final String sqlTemplate;
|
||||
|
||||
/**
|
||||
* 不满足 sqlTemplate 则填充
|
||||
*/
|
||||
private final String elseSql;
|
||||
|
||||
public static DataScopeType findCode(String code) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return null;
|
||||
}
|
||||
for (DataScopeType type : values()) {
|
||||
if (type.getCode().equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package org.ruoyi.common.mybatis.handler;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.ruoyi.common.core.domain.model.LoginUser;
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MP注入处理器
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2021/4/25
|
||||
*/
|
||||
@Slf4j
|
||||
public class InjectionMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
try {
|
||||
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
|
||||
Date current = ObjectUtil.isNotNull(baseEntity.getCreateTime())
|
||||
? baseEntity.getCreateTime() : new Date();
|
||||
baseEntity.setCreateTime(current);
|
||||
baseEntity.setUpdateTime(current);
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (ObjectUtil.isNotNull(loginUser)) {
|
||||
Long userId = ObjectUtil.isNotNull(baseEntity.getCreateBy())
|
||||
? baseEntity.getCreateBy() : loginUser.getUserId();
|
||||
// 当前已登录 且 创建人为空 则填充
|
||||
baseEntity.setCreateBy(userId);
|
||||
// 当前已登录 且 更新人为空 则填充
|
||||
baseEntity.setUpdateBy(userId);
|
||||
baseEntity.setCreateDept(ObjectUtil.isNotNull(baseEntity.getCreateDept())
|
||||
? baseEntity.getCreateDept() : loginUser.getDeptId());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
try {
|
||||
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
|
||||
Date current = new Date();
|
||||
// 更新时间填充(不管为不为空)
|
||||
baseEntity.setUpdateTime(current);
|
||||
LoginUser loginUser = getLoginUser();
|
||||
// 当前已登录 更新人填充(不管为不为空)
|
||||
if (ObjectUtil.isNotNull(loginUser)) {
|
||||
baseEntity.setUpdateBy(loginUser.getUserId());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录用户名
|
||||
*/
|
||||
private LoginUser getLoginUser() {
|
||||
LoginUser loginUser;
|
||||
try {
|
||||
loginUser = LoginHelper.getLoginUser();
|
||||
} catch (Exception e) {
|
||||
log.warn("自动注入警告 => 用户未登录");
|
||||
return null;
|
||||
}
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package org.ruoyi.common.mybatis.handler;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.collection.ConcurrentHashSet;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.Parenthesis;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import org.ruoyi.common.core.domain.dto.RoleDTO;
|
||||
import org.ruoyi.common.core.domain.model.LoginUser;
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.core.utils.StreamUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.mybatis.annotation.DataColumn;
|
||||
import org.ruoyi.common.mybatis.annotation.DataPermission;
|
||||
import org.ruoyi.common.mybatis.enums.DataScopeType;
|
||||
import org.ruoyi.common.mybatis.helper.DataPermissionHelper;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.expression.BeanResolver;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.ParserContext;
|
||||
import org.springframework.expression.common.TemplateParserContext;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 数据权限过滤
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class PlusDataPermissionHandler {
|
||||
|
||||
/**
|
||||
* 方法或类(名称) 与 注解的映射关系缓存
|
||||
*/
|
||||
private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 无效注解方法缓存用于快速返回
|
||||
*/
|
||||
private final Set<String> invalidCacheSet = new ConcurrentHashSet<>();
|
||||
|
||||
/**
|
||||
* spel 解析器
|
||||
*/
|
||||
private final ExpressionParser parser = new SpelExpressionParser();
|
||||
private final ParserContext parserContext = new TemplateParserContext();
|
||||
/**
|
||||
* bean解析器 用于处理 spel 表达式中对 bean 的调用
|
||||
*/
|
||||
private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
|
||||
|
||||
|
||||
public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
|
||||
DataColumn[] dataColumns = findAnnotation(mappedStatementId);
|
||||
if (ArrayUtil.isEmpty(dataColumns)) {
|
||||
invalidCacheSet.add(mappedStatementId);
|
||||
return where;
|
||||
}
|
||||
LoginUser currentUser = DataPermissionHelper.getVariable("user");
|
||||
if (ObjectUtil.isNull(currentUser)) {
|
||||
currentUser = LoginHelper.getLoginUser();
|
||||
DataPermissionHelper.setVariable("user", currentUser);
|
||||
}
|
||||
// 如果是超级管理员或租户管理员,则不过滤数据
|
||||
if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
|
||||
return where;
|
||||
}
|
||||
String dataFilterSql = buildDataFilter(dataColumns, isSelect);
|
||||
if (StringUtils.isBlank(dataFilterSql)) {
|
||||
return where;
|
||||
}
|
||||
try {
|
||||
Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
|
||||
// 数据权限使用单独的括号 防止与其他条件冲突
|
||||
Parenthesis parenthesis = new Parenthesis(expression);
|
||||
if (ObjectUtil.isNotNull(where)) {
|
||||
return new AndExpression(where, parenthesis);
|
||||
} else {
|
||||
return parenthesis;
|
||||
}
|
||||
} catch (JSQLParserException e) {
|
||||
throw new ServiceException("数据权限解析异常 => " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造数据过滤sql
|
||||
*/
|
||||
private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
|
||||
// 更新或删除需满足所有条件
|
||||
String joinStr = isSelect ? " OR " : " AND ";
|
||||
LoginUser user = DataPermissionHelper.getVariable("user");
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
context.setBeanResolver(beanResolver);
|
||||
DataPermissionHelper.getContext().forEach(context::setVariable);
|
||||
Set<String> conditions = new HashSet<>();
|
||||
for (RoleDTO role : user.getRoles()) {
|
||||
user.setRoleId(role.getRoleId());
|
||||
// 获取角色权限泛型
|
||||
DataScopeType type = DataScopeType.findCode(role.getDataScope());
|
||||
if (ObjectUtil.isNull(type)) {
|
||||
throw new ServiceException("角色数据范围异常 => " + role.getDataScope());
|
||||
}
|
||||
// 全部数据权限直接返回
|
||||
if (type == DataScopeType.ALL) {
|
||||
return "";
|
||||
}
|
||||
boolean isSuccess = false;
|
||||
for (DataColumn dataColumn : dataColumns) {
|
||||
if (dataColumn.key().length != dataColumn.value().length) {
|
||||
throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
|
||||
}
|
||||
// 不包含 key 变量 则不处理
|
||||
if (!StringUtils.containsAny(type.getSqlTemplate(),
|
||||
Arrays.stream(dataColumn.key()).map(key -> "#" + key).toArray(String[]::new)
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
// 设置注解变量 key 为表达式变量 value 为变量值
|
||||
for (int i = 0; i < dataColumn.key().length; i++) {
|
||||
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
|
||||
}
|
||||
|
||||
// 解析sql模板并填充
|
||||
String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);
|
||||
conditions.add(joinStr + sql);
|
||||
isSuccess = true;
|
||||
}
|
||||
// 未处理成功则填充兜底方案
|
||||
if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
|
||||
conditions.add(joinStr + type.getElseSql());
|
||||
}
|
||||
}
|
||||
|
||||
if (CollUtil.isNotEmpty(conditions)) {
|
||||
String sql = StreamUtils.join(conditions, Function.identity(), "");
|
||||
return sql.substring(joinStr.length());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private DataColumn[] findAnnotation(String mappedStatementId) {
|
||||
StringBuilder sb = new StringBuilder(mappedStatementId);
|
||||
int index = sb.lastIndexOf(".");
|
||||
String clazzName = sb.substring(0, index);
|
||||
String methodName = sb.substring(index + 1, sb.length());
|
||||
Class<?> clazz = ClassUtil.loadClass(clazzName);
|
||||
List<Method> methods = Arrays.stream(ClassUtil.getDeclaredMethods(clazz))
|
||||
.filter(method -> method.getName().equals(methodName)).toList();
|
||||
DataPermission dataPermission;
|
||||
// 获取方法注解
|
||||
for (Method method : methods) {
|
||||
dataPermission = dataPermissionCacheMap.get(mappedStatementId);
|
||||
if (ObjectUtil.isNotNull(dataPermission)) {
|
||||
return dataPermission.value();
|
||||
}
|
||||
if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
|
||||
dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
|
||||
dataPermissionCacheMap.put(mappedStatementId, dataPermission);
|
||||
return dataPermission.value();
|
||||
}
|
||||
}
|
||||
dataPermission = dataPermissionCacheMap.get(clazz.getName());
|
||||
if (ObjectUtil.isNotNull(dataPermission)) {
|
||||
return dataPermission.value();
|
||||
}
|
||||
// 获取类注解
|
||||
if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
|
||||
dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
|
||||
dataPermissionCacheMap.put(clazz.getName(), dataPermission);
|
||||
return dataPermission.value();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为无效方法 无数据权限
|
||||
*/
|
||||
public boolean isInvalid(String mappedStatementId) {
|
||||
return invalidCacheSet.contains(mappedStatementId);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.ruoyi.common.mybatis.helper;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.context.model.SaStorage;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
|
||||
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 数据权限助手
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("unchecked cast")
|
||||
public class DataPermissionHelper {
|
||||
|
||||
private static final String DATA_PERMISSION_KEY = "data:permission";
|
||||
|
||||
public static <T> T getVariable(String key) {
|
||||
Map<String, Object> context = getContext();
|
||||
return (T) context.get(key);
|
||||
}
|
||||
|
||||
|
||||
public static void setVariable(String key, Object value) {
|
||||
Map<String, Object> context = getContext();
|
||||
context.put(key, value);
|
||||
}
|
||||
|
||||
public static Map<String, Object> getContext() {
|
||||
SaStorage saStorage = SaHolder.getStorage();
|
||||
Object attribute = saStorage.get(DATA_PERMISSION_KEY);
|
||||
if (ObjectUtil.isNull(attribute)) {
|
||||
saStorage.set(DATA_PERMISSION_KEY, new HashMap<>());
|
||||
attribute = saStorage.get(DATA_PERMISSION_KEY);
|
||||
}
|
||||
if (attribute instanceof Map map) {
|
||||
return map;
|
||||
}
|
||||
throw new NullPointerException("data permission context type exception");
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
|
||||
*/
|
||||
public static void enableIgnore() {
|
||||
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭忽略数据权限
|
||||
*/
|
||||
public static void disableIgnore() {
|
||||
InterceptorIgnoreHelper.clearIgnoreStrategy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在忽略数据权限中执行
|
||||
*
|
||||
* @param handle 处理执行方法
|
||||
*/
|
||||
public static void ignore(Runnable handle) {
|
||||
enableIgnore();
|
||||
try {
|
||||
handle.run();
|
||||
} finally {
|
||||
disableIgnore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在忽略数据权限中执行
|
||||
*
|
||||
* @param handle 处理执行方法
|
||||
*/
|
||||
public static <T> T ignore(Supplier<T> handle) {
|
||||
enableIgnore();
|
||||
try {
|
||||
return handle.get();
|
||||
} finally {
|
||||
disableIgnore();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package org.ruoyi.common.mybatis.interceptor;
|
||||
|
||||
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.statement.delete.Delete;
|
||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||
import net.sf.jsqlparser.statement.select.Select;
|
||||
import net.sf.jsqlparser.statement.select.SelectBody;
|
||||
import net.sf.jsqlparser.statement.select.SetOperationList;
|
||||
import net.sf.jsqlparser.statement.update.Update;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlCommandType;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
import org.ruoyi.common.mybatis.handler.PlusDataPermissionHandler;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据权限拦截器
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
public class PlusDataPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {
|
||||
|
||||
private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();
|
||||
|
||||
@Override
|
||||
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
|
||||
// 检查忽略注解
|
||||
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
// 检查是否无效 无数据权限注解
|
||||
if (dataPermissionHandler.isInvalid(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
// 解析 sql 分配对应方法
|
||||
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
|
||||
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
|
||||
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
|
||||
MappedStatement ms = mpSh.mappedStatement();
|
||||
SqlCommandType sct = ms.getSqlCommandType();
|
||||
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
|
||||
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
|
||||
mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processSelect(Select select, int index, String sql, Object obj) {
|
||||
SelectBody selectBody = select.getSelectBody();
|
||||
if (selectBody instanceof PlainSelect plainSelect) {
|
||||
this.setWhere(plainSelect, (String) obj);
|
||||
} else if (selectBody instanceof SetOperationList setOperationList) {
|
||||
List<SelectBody> selectBodyList = setOperationList.getSelects();
|
||||
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processUpdate(Update update, int index, String sql, Object obj) {
|
||||
Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
|
||||
if (null != sqlSegment) {
|
||||
update.setWhere(sqlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processDelete(Delete delete, int index, String sql, Object obj) {
|
||||
Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
|
||||
if (null != sqlSegment) {
|
||||
delete.setWhere(sqlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 where 条件
|
||||
*
|
||||
* @param plainSelect 查询对象
|
||||
* @param mappedStatementId 执行方法id
|
||||
*/
|
||||
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
|
||||
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
|
||||
if (null != sqlSegment) {
|
||||
plainSelect.setWhere(sqlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.ruoyi.common.mybatis.jakarta;
|
||||
|
||||
import com.baomidou.dynamic.datasource.processor.DsProcessor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
/**
|
||||
* @author TaoYu
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public class DsJakartaHeaderProcessor extends DsProcessor {
|
||||
|
||||
/**
|
||||
* header prefix
|
||||
*/
|
||||
private static final String HEADER_PREFIX = "#header";
|
||||
|
||||
@Override
|
||||
public boolean matches(String key) {
|
||||
return key.startsWith(HEADER_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String doDetermineDatasource(MethodInvocation invocation, String key) {
|
||||
HttpServletRequest request = (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
return request.getHeader(key.substring(8));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.ruoyi.common.mybatis.jakarta;
|
||||
|
||||
import com.baomidou.dynamic.datasource.processor.DsProcessor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
|
||||
/**
|
||||
* @author TaoYu
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public class DsJakartaSessionProcessor extends DsProcessor {
|
||||
|
||||
/**
|
||||
* session开头
|
||||
*/
|
||||
private static final String SESSION_PREFIX = "#session";
|
||||
|
||||
@Override
|
||||
public boolean matches(String key) {
|
||||
return key.startsWith(SESSION_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String doDetermineDatasource(MethodInvocation invocation, String key) {
|
||||
HttpServletRequest request = (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
return request.getSession().getAttribute(key.substring(9)).toString();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
package org.ruoyi.common.mybatis.config;
|
||||
package org.ruoyi.config;
|
||||
|
||||
import cn.hutool.core.net.NetUtil;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
|
||||
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.ruoyi.common.mybatis.handler.InjectionMetaObjectHandler;
|
||||
import org.ruoyi.common.mybatis.interceptor.PlusDataPermissionInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import org.ruoyi.aspect.DataPermissionAspect;
|
||||
import org.ruoyi.common.core.factory.YmlPropertySourceFactory;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.handler.InjectionMetaObjectHandler;
|
||||
import org.ruoyi.handler.MybatisExceptionHandler;
|
||||
import org.ruoyi.handler.PlusPostInitTableInfoHandler;
|
||||
import org.ruoyi.interceptor.PlusDataPermissionInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
@@ -20,13 +28,19 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
* @author Lion Li
|
||||
*/
|
||||
@EnableTransactionManagement(proxyTargetClass = true)
|
||||
@AutoConfiguration
|
||||
@MapperScan("${mybatis-plus.mapperPackage}")
|
||||
@PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 多租户插件 必须放到第一位
|
||||
try {
|
||||
TenantLineInnerInterceptor tenant = SpringUtils.getBean(TenantLineInnerInterceptor.class);
|
||||
interceptor.addInnerInterceptor(tenant);
|
||||
} catch (BeansException ignore) {
|
||||
}
|
||||
// 数据权限处理
|
||||
interceptor.addInnerInterceptor(dataPermissionInterceptor());
|
||||
// 分页插件
|
||||
@@ -40,7 +54,15 @@ public class MybatisPlusConfig {
|
||||
* 数据权限拦截器
|
||||
*/
|
||||
public PlusDataPermissionInterceptor dataPermissionInterceptor() {
|
||||
return new PlusDataPermissionInterceptor();
|
||||
return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据权限切面处理器
|
||||
*/
|
||||
@Bean
|
||||
public DataPermissionAspect dataPermissionAspect() {
|
||||
return new DataPermissionAspect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,8 +70,6 @@ public class MybatisPlusConfig {
|
||||
*/
|
||||
public PaginationInnerInterceptor paginationInnerInterceptor() {
|
||||
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
|
||||
// 设置最大单页限制数量,默认 500 条,-1 不受限制
|
||||
paginationInnerInterceptor.setMaxLimit(-1L);
|
||||
// 分页合理化
|
||||
paginationInnerInterceptor.setOverflow(true);
|
||||
return paginationInnerInterceptor;
|
||||
@@ -79,6 +99,22 @@ public class MybatisPlusConfig {
|
||||
return new DefaultIdentifierGenerator(NetUtil.getLocalhost());
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常处理器
|
||||
*/
|
||||
@Bean
|
||||
public MybatisExceptionHandler mybatisExceptionHandler() {
|
||||
return new MybatisExceptionHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表对象处理器
|
||||
*/
|
||||
@Bean
|
||||
public PostInitTableInfoHandler postInitTableInfoHandler() {
|
||||
return new PlusPostInitTableInfoHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* PaginationInnerInterceptor 分页插件,自动识别数据库类型
|
||||
* https://baomidou.com/pages/97710a/
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.ruoyi.common.mybatis.core.domain;
|
||||
package org.ruoyi.core.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
@@ -17,7 +17,6 @@ import java.util.Map;
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
package org.ruoyi.core.mapper;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.Db;
|
||||
import org.apache.ibatis.logging.Log;
|
||||
import org.apache.ibatis.logging.LogFactory;
|
||||
import org.ruoyi.common.core.utils.MapstructUtils;
|
||||
import org.ruoyi.common.core.utils.StreamUtils;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 自定义 Mapper 接口, 实现 自定义扩展
|
||||
*
|
||||
* @param <T> table 泛型
|
||||
* @param <V> vo 泛型
|
||||
* @author Lion Li
|
||||
* @since 2021-05-13
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
|
||||
Log log = LogFactory.getLog(BaseMapperPlus.class);
|
||||
|
||||
/**
|
||||
* 获取当前实例对象关联的泛型类型 V 的 Class 对象
|
||||
*
|
||||
* @return 返回当前实例对象关联的泛型类型 V 的 Class 对象
|
||||
*/
|
||||
default Class<V> currentVoClass() {
|
||||
return (Class<V>) GenericTypeUtils.resolveTypeArguments(this.getClass(), BaseMapperPlus.class)[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前实例对象关联的泛型类型 T 的 Class 对象
|
||||
*
|
||||
* @return 返回当前实例对象关联的泛型类型 T 的 Class 对象
|
||||
*/
|
||||
default Class<T> currentModelClass() {
|
||||
return (Class<T>) GenericTypeUtils.resolveTypeArguments(this.getClass(), BaseMapperPlus.class)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的查询条件查询并返回结果列表
|
||||
*
|
||||
* @return 返回查询结果的列表
|
||||
*/
|
||||
default List<T> selectList() {
|
||||
return this.selectList(new QueryWrapper<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入实体对象集合
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @return 插入操作是否成功的布尔值
|
||||
*/
|
||||
default boolean insertBatch(Collection<T> entityList) {
|
||||
return Db.saveBatch(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量根据ID更新实体对象集合
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @return 更新操作是否成功的布尔值
|
||||
*/
|
||||
default boolean updateBatchById(Collection<T> entityList) {
|
||||
return Db.updateBatchById(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入或更新实体对象集合
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @return 插入或更新操作是否成功的布尔值
|
||||
*/
|
||||
default boolean insertOrUpdateBatch(Collection<T> entityList) {
|
||||
return Db.saveOrUpdateBatch(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入实体对象集合并指定批处理大小
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 批处理大小
|
||||
* @return 插入操作是否成功的布尔值
|
||||
*/
|
||||
default boolean insertBatch(Collection<T> entityList, int batchSize) {
|
||||
return Db.saveBatch(entityList, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量根据ID更新实体对象集合并指定批处理大小
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 批处理大小
|
||||
* @return 更新操作是否成功的布尔值
|
||||
*/
|
||||
default boolean updateBatchById(Collection<T> entityList, int batchSize) {
|
||||
return Db.updateBatchById(entityList, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入或更新实体对象集合并指定批处理大小
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 批处理大小
|
||||
* @return 插入或更新操作是否成功的布尔值
|
||||
*/
|
||||
default boolean insertOrUpdateBatch(Collection<T> entityList, int batchSize) {
|
||||
return Db.saveOrUpdateBatch(entityList, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询单个VO对象
|
||||
*
|
||||
* @param id 主键ID
|
||||
* @return 查询到的单个VO对象
|
||||
*/
|
||||
default V selectVoById(Serializable id) {
|
||||
return selectVoById(id, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询单个VO对象并将其转换为指定的VO类
|
||||
*
|
||||
* @param id 主键ID
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的单个VO对象,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> C selectVoById(Serializable id, Class<C> voClass) {
|
||||
T obj = this.selectById(id);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return null;
|
||||
}
|
||||
return MapstructUtils.convert(obj, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID集合批量查询VO对象列表
|
||||
*
|
||||
* @param idList 主键ID集合
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoByIds(Collection<? extends Serializable> idList) {
|
||||
return selectVoByIds(idList, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID集合批量查询实体对象列表,并将其转换为指定的VO对象列表
|
||||
*
|
||||
* @param idList 主键ID集合
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的VO对象列表,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> List<C> selectVoByIds(Collection<? extends Serializable> idList, Class<C> voClass) {
|
||||
List<T> list = this.selectByIds(idList);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return MapstructUtils.convert(list, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据查询条件Map查询VO对象列表
|
||||
*
|
||||
* @param map 查询条件Map
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoByMap(Map<String, Object> map) {
|
||||
return selectVoByMap(map, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据查询条件Map查询实体对象列表,并将其转换为指定的VO对象列表
|
||||
*
|
||||
* @param map 查询条件Map
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的VO对象列表,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> List<C> selectVoByMap(Map<String, Object> map, Class<C> voClass) {
|
||||
List<T> list = this.selectByMap(map);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return MapstructUtils.convert(list, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个VO对象
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @return 查询到的单个VO对象
|
||||
*/
|
||||
default V selectVoOne(Wrapper<T> wrapper) {
|
||||
return selectVoOne(wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个VO对象,并根据需要决定是否抛出异常
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param throwEx 是否抛出异常的标志
|
||||
* @return 查询到的单个VO对象
|
||||
*/
|
||||
default V selectVoOne(Wrapper<T> wrapper, boolean throwEx) {
|
||||
return selectVoOne(wrapper, this.currentVoClass(), throwEx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个VO对象,并指定返回的VO对象的类型
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 返回的VO对象的Class对象
|
||||
* @param <C> 返回的VO对象的类型
|
||||
* @return 查询到的单个VO对象,经过类型转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass) {
|
||||
return selectVoOne(wrapper, voClass, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个实体对象,并将其转换为指定的VO对象
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param throwEx 是否抛出异常的标志
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的单个VO对象,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass, boolean throwEx) {
|
||||
T obj = this.selectOne(wrapper, throwEx);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return null;
|
||||
}
|
||||
return MapstructUtils.convert(obj, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有VO对象列表
|
||||
*
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoList() {
|
||||
return selectVoList(new QueryWrapper<>(), this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询VO对象列表
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoList(Wrapper<T> wrapper) {
|
||||
return selectVoList(wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询实体对象列表,并将其转换为指定的VO对象列表
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的VO对象列表,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> List<C> selectVoList(Wrapper<T> wrapper, Class<C> voClass) {
|
||||
List<T> list = this.selectList(wrapper);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return MapstructUtils.convert(list, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件分页查询VO对象列表
|
||||
*
|
||||
* @param page 分页信息
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @return 查询到的VO对象分页列表
|
||||
*/
|
||||
default <P extends IPage<V>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper) {
|
||||
return selectVoPage(page, wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件分页查询实体对象列表,并将其转换为指定的VO对象分页列表
|
||||
*
|
||||
* @param page 分页信息
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @param <P> VO对象分页列表的类型
|
||||
* @return 查询到的VO对象分页列表,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C, P extends IPage<C>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper, Class<C> voClass) {
|
||||
// 根据条件分页查询实体对象列表
|
||||
List<T> list = this.selectList(page, wrapper);
|
||||
// 创建一个新的VO对象分页列表,并设置分页信息
|
||||
IPage<C> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return (P) voPage;
|
||||
}
|
||||
voPage.setRecords(MapstructUtils.convert(list, voClass));
|
||||
return (P) voPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param mapper 转换函数,用于将查询到的对象转换为指定类型的对象
|
||||
* @param <C> 要转换的对象的类型
|
||||
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
|
||||
*/
|
||||
default <C> List<C> selectObjs(Wrapper<T> wrapper, Function<? super Object, C> mapper) {
|
||||
return StreamUtils.toList(this.selectObjs(wrapper), mapper);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.ruoyi.common.mybatis.core.page;
|
||||
package org.ruoyi.core.page;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
@@ -19,7 +20,6 @@ import java.util.List;
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
|
||||
@Data
|
||||
public class PageQuery implements Serializable {
|
||||
|
||||
@@ -56,6 +56,9 @@ public class PageQuery implements Serializable {
|
||||
*/
|
||||
public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* 构建分页对象
|
||||
*/
|
||||
public <T> Page<T> build() {
|
||||
Integer pageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
|
||||
Integer pageSize = ObjectUtil.defaultIfNull(getPageSize(), DEFAULT_PAGE_SIZE);
|
||||
@@ -111,4 +114,14 @@ public class PageQuery implements Serializable {
|
||||
return list;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Integer getFirstNum() {
|
||||
return (pageNum - 1) * pageSize;
|
||||
}
|
||||
|
||||
public PageQuery(Integer pageSize, Integer pageNum) {
|
||||
this.pageSize = pageSize;
|
||||
this.pageNum = pageNum;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.ruoyi.common.mybatis.core.page;
|
||||
package org.ruoyi.core.page;
|
||||
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
@@ -14,7 +14,6 @@ import java.util.List;
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class TableDataInfo<T> implements Serializable {
|
||||
@@ -51,8 +50,13 @@ public class TableDataInfo<T> implements Serializable {
|
||||
public TableDataInfo(List<T> list, long total) {
|
||||
this.rows = list;
|
||||
this.total = total;
|
||||
this.code = HttpStatus.HTTP_OK;
|
||||
this.msg = "查询成功";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分页对象构建表格分页数据对象
|
||||
*/
|
||||
public static <T> TableDataInfo<T> build(IPage<T> page) {
|
||||
TableDataInfo<T> rspData = new TableDataInfo<>();
|
||||
rspData.setCode(HttpStatus.HTTP_OK);
|
||||
@@ -62,6 +66,9 @@ public class TableDataInfo<T> implements Serializable {
|
||||
return rspData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据列表构建表格分页数据对象
|
||||
*/
|
||||
public static <T> TableDataInfo<T> build(List<T> list) {
|
||||
TableDataInfo<T> rspData = new TableDataInfo<>();
|
||||
rspData.setCode(HttpStatus.HTTP_OK);
|
||||
@@ -71,6 +78,9 @@ public class TableDataInfo<T> implements Serializable {
|
||||
return rspData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建表格分页数据对象
|
||||
*/
|
||||
public static <T> TableDataInfo<T> build() {
|
||||
TableDataInfo<T> rspData = new TableDataInfo<>();
|
||||
rspData.setCode(HttpStatus.HTTP_OK);
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.ruoyi.common.mybatis.enums;
|
||||
package org.ruoyi.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
|
||||
|
||||
/**
|
||||
* 数据库类型
|
||||
*
|
||||
@@ -33,8 +34,17 @@ public enum DataBaseType {
|
||||
*/
|
||||
SQL_SERVER("Microsoft SQL Server");
|
||||
|
||||
/**
|
||||
* 数据库类型
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
/**
|
||||
* 根据数据库产品名称查找对应的数据库类型
|
||||
*
|
||||
* @param databaseProductName 数据库产品名称
|
||||
* @return 对应的数据库类型枚举值,如果未找到则返回 null
|
||||
*/
|
||||
public static DataBaseType find(String databaseProductName) {
|
||||
if (StringUtils.isBlank(databaseProductName)) {
|
||||
return null;
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.ruoyi.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.ruoyi.common.core.domain.model.LoginUser;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.helper.DataPermissionHelper;
|
||||
|
||||
/**
|
||||
* 数据权限类型枚举
|
||||
* <p>
|
||||
* 支持使用 SpEL 模板表达式定义 SQL 查询条件
|
||||
* 内置数据:
|
||||
* - {@code user}: 当前登录用户信息,参考 {@link LoginUser}
|
||||
* 内置服务:
|
||||
* - {@code sdss}: 系统数据权限服务,参考 ISysDataScopeService
|
||||
* 如需扩展数据,可以通过 {@link DataPermissionHelper} 进行操作
|
||||
* 如需扩展服务,可以通过 ISysDataScopeService 自行编写
|
||||
* </p>
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DataScopeType {
|
||||
|
||||
/**
|
||||
* 全部数据权限
|
||||
*/
|
||||
ALL("1", "", ""),
|
||||
|
||||
/**
|
||||
* 自定数据权限
|
||||
*/
|
||||
CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", " 1 = 0 "),
|
||||
|
||||
/**
|
||||
* 部门数据权限
|
||||
*/
|
||||
DEPT("3", " #{#deptName} = #{#user.deptId} ", " 1 = 0 "),
|
||||
|
||||
/**
|
||||
* 部门及以下数据权限
|
||||
*/
|
||||
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", " 1 = 0 "),
|
||||
|
||||
/**
|
||||
* 仅本人数据权限
|
||||
*/
|
||||
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 "),
|
||||
|
||||
/**
|
||||
* 部门及以下或本人数据权限
|
||||
*/
|
||||
DEPT_AND_CHILD_OR_SELF("6", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} ) OR #{#userName} = #{#user.userId} ", " 1 = 0 ");
|
||||
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
* SpEL 模板表达式,用于构建 SQL 查询条件
|
||||
*/
|
||||
private final String sqlTemplate;
|
||||
|
||||
/**
|
||||
* 如果不满足 {@code sqlTemplate} 的条件,则使用此默认 SQL 表达式
|
||||
*/
|
||||
private final String elseSql;
|
||||
|
||||
/**
|
||||
* 根据枚举代码查找对应的枚举值
|
||||
*
|
||||
* @param code 枚举代码
|
||||
* @return 对应的枚举值,如果未找到则返回 null
|
||||
*/
|
||||
public static DataScopeType findCode(String code) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return null;
|
||||
}
|
||||
for (DataScopeType type : values()) {
|
||||
if (type.getCode().equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.ruoyi.handler;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.ruoyi.common.core.domain.model.LoginUser;
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.common.core.utils.ObjectUtils;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.core.domain.BaseEntity;
|
||||
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MP注入处理器
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2021/4/25
|
||||
*/
|
||||
@Slf4j
|
||||
public class InjectionMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 插入填充方法,用于在插入数据时自动填充实体对象中的创建时间、更新时间、创建人、更新人等信息
|
||||
*
|
||||
* @param metaObject 元对象,用于获取原始对象并进行填充
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
try {
|
||||
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
|
||||
// 获取当前时间作为创建时间和更新时间,如果创建时间不为空,则使用创建时间,否则使用当前时间
|
||||
Date current = ObjectUtils.notNull(baseEntity.getCreateTime(), new Date());
|
||||
baseEntity.setCreateTime(current);
|
||||
baseEntity.setUpdateTime(current);
|
||||
|
||||
// 如果创建人为空,则填充当前登录用户的信息
|
||||
if (ObjectUtil.isNull(baseEntity.getCreateBy())) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (ObjectUtil.isNotNull(loginUser)) {
|
||||
Long userId = loginUser.getUserId();
|
||||
// 填充创建人、更新人和创建部门信息
|
||||
baseEntity.setCreateBy(userId);
|
||||
baseEntity.setUpdateBy(userId);
|
||||
baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), loginUser.getDeptId()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Date date = new Date();
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, date);
|
||||
this.strictInsertFill(metaObject, "updateTime", Date.class, date);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新填充方法,用于在更新数据时自动填充实体对象中的更新时间和更新人信息
|
||||
*
|
||||
* @param metaObject 元对象,用于获取原始对象并进行填充
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
try {
|
||||
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
|
||||
// 获取当前时间作为更新时间,无论原始对象中的更新时间是否为空都填充
|
||||
Date current = new Date();
|
||||
baseEntity.setUpdateTime(current);
|
||||
|
||||
// 获取当前登录用户的ID,并填充更新人信息
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (ObjectUtil.isNotNull(userId)) {
|
||||
baseEntity.setUpdateBy(userId);
|
||||
}
|
||||
} else {
|
||||
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*
|
||||
* @return 当前登录用户的信息,如果用户未登录则返回 null
|
||||
*/
|
||||
private LoginUser getLoginUser() {
|
||||
LoginUser loginUser;
|
||||
try {
|
||||
loginUser = LoginHelper.getLoginUser();
|
||||
} catch (Exception e) {
|
||||
log.warn("自动注入警告 => 用户未登录");
|
||||
return null;
|
||||
}
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.ruoyi.common.mybatis.handler;
|
||||
package org.ruoyi.handler;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
|
||||
import org.mybatis.spring.MyBatisSystemException;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
@@ -34,7 +36,7 @@ public class MybatisExceptionHandler {
|
||||
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
String message = e.getMessage();
|
||||
if (message.contains("CannotFindDataSourceException")) {
|
||||
if (StringUtils.contains("CannotFindDataSourceException", message)) {
|
||||
log.error("请求地址'{}', 未找到数据源", requestURI);
|
||||
return R.fail("未找到数据源,请联系管理员确认");
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
package org.ruoyi.handler;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import org.apache.ibatis.io.Resources;
|
||||
|
||||
import org.ruoyi.annotation.DataColumn;
|
||||
import org.ruoyi.annotation.DataPermission;
|
||||
import org.ruoyi.common.core.domain.dto.RoleDTO;
|
||||
import org.ruoyi.common.core.domain.model.LoginUser;
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.core.utils.StreamUtils;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.enums.DataScopeType;
|
||||
import org.ruoyi.helper.DataPermissionHelper;
|
||||
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.core.type.ClassMetadata;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.expression.*;
|
||||
import org.springframework.expression.common.TemplateParserContext;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 数据权限过滤
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class PlusDataPermissionHandler {
|
||||
|
||||
/**
|
||||
* 类名称与注解的映射关系缓存(由于aop无法拦截mybatis接口类上的注解 只能通过启动预扫描的方式进行)
|
||||
*/
|
||||
private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* spel 解析器
|
||||
*/
|
||||
private final ExpressionParser parser = new SpelExpressionParser();
|
||||
private final ParserContext parserContext = new TemplateParserContext();
|
||||
/**
|
||||
* bean解析器 用于处理 spel 表达式中对 bean 的调用
|
||||
*/
|
||||
private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
|
||||
|
||||
/**
|
||||
* 构造方法,扫描指定包下的 Mapper 类并初始化缓存
|
||||
*
|
||||
* @param mapperPackage Mapper 类所在的包路径
|
||||
*/
|
||||
public PlusDataPermissionHandler(String mapperPackage) {
|
||||
scanMapperClasses(mapperPackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据过滤条件的 SQL 片段
|
||||
*
|
||||
* @param where 原始的查询条件表达式
|
||||
* @param mappedStatementId Mapper 方法的 ID
|
||||
* @param isSelect 是否为查询语句
|
||||
* @return 数据过滤条件的 SQL 片段
|
||||
*/
|
||||
public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
|
||||
try {
|
||||
// 获取数据权限配置
|
||||
DataPermission dataPermission = getDataPermission(mappedStatementId);
|
||||
// 获取当前登录用户信息
|
||||
LoginUser currentUser = DataPermissionHelper.getVariable("user");
|
||||
if (ObjectUtil.isNull(currentUser)) {
|
||||
currentUser = LoginHelper.getLoginUser();
|
||||
DataPermissionHelper.setVariable("user", currentUser);
|
||||
}
|
||||
// 如果是超级管理员或租户管理员,则不过滤数据
|
||||
if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
|
||||
return where;
|
||||
}
|
||||
// 构造数据过滤条件的 SQL 片段
|
||||
String dataFilterSql = buildDataFilter(dataPermission, isSelect);
|
||||
if (StringUtils.isBlank(dataFilterSql)) {
|
||||
return where;
|
||||
}
|
||||
Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
|
||||
// 数据权限使用单独的括号 防止与其他条件冲突
|
||||
ParenthesedExpressionList<Expression> parenthesis = new ParenthesedExpressionList<>(expression);
|
||||
if (ObjectUtil.isNotNull(where)) {
|
||||
return new AndExpression(where, parenthesis);
|
||||
} else {
|
||||
return parenthesis;
|
||||
}
|
||||
} catch (JSQLParserException e) {
|
||||
throw new ServiceException("数据权限解析异常 => " + e.getMessage());
|
||||
} finally {
|
||||
DataPermissionHelper.removePermission();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建数据过滤条件的 SQL 语句
|
||||
*
|
||||
* @param dataPermission 数据权限注解
|
||||
* @param isSelect 标志当前操作是否为查询操作,查询操作和更新或删除操作在处理过滤条件时会有不同的处理方式
|
||||
* @return 构建的数据过滤条件的 SQL 语句
|
||||
* @throws ServiceException 如果角色的数据范围异常或者 key 与 value 的长度不匹配,则抛出 ServiceException 异常
|
||||
*/
|
||||
private String buildDataFilter(DataPermission dataPermission, boolean isSelect) {
|
||||
// 更新或删除需满足所有条件
|
||||
String joinStr = isSelect ? " OR " : " AND ";
|
||||
if (StringUtils.isNotBlank(dataPermission.joinStr())) {
|
||||
joinStr = " " + dataPermission.joinStr() + " ";
|
||||
}
|
||||
LoginUser user = DataPermissionHelper.getVariable("user");
|
||||
Object defaultValue = "-1";
|
||||
NullSafeStandardEvaluationContext context = new NullSafeStandardEvaluationContext(defaultValue);
|
||||
context.addPropertyAccessor(new NullSafePropertyAccessor(context.getPropertyAccessors().get(0), defaultValue));
|
||||
context.setBeanResolver(beanResolver);
|
||||
DataPermissionHelper.getContext().forEach(context::setVariable);
|
||||
Set<String> conditions = new HashSet<>();
|
||||
// 优先设置变量
|
||||
List<String> keys = new ArrayList<>();
|
||||
Map<DataColumn, Boolean> ignoreMap = new HashMap<>();
|
||||
for (DataColumn dataColumn : dataPermission.value()) {
|
||||
if (dataColumn.key().length != dataColumn.value().length) {
|
||||
throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
|
||||
}
|
||||
// 包含权限标识符 这直接跳过
|
||||
if (StringUtils.isNotBlank(dataColumn.permission()) &&
|
||||
CollUtil.contains(user.getMenuPermission(), dataColumn.permission())
|
||||
) {
|
||||
ignoreMap.put(dataColumn, Boolean.TRUE);
|
||||
continue;
|
||||
}
|
||||
// 设置注解变量 key 为表达式变量 value 为变量值
|
||||
for (int i = 0; i < dataColumn.key().length; i++) {
|
||||
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
|
||||
}
|
||||
keys.addAll(Arrays.stream(dataColumn.key()).map(key -> "#" + key).toList());
|
||||
}
|
||||
|
||||
for (RoleDTO role : user.getRoles()) {
|
||||
user.setRoleId(role.getRoleId());
|
||||
// 获取角色权限泛型
|
||||
DataScopeType type = DataScopeType.findCode(role.getDataScope());
|
||||
if (ObjectUtil.isNull(type)) {
|
||||
throw new ServiceException("角色数据范围异常 => " + role.getDataScope());
|
||||
}
|
||||
// 全部数据权限直接返回
|
||||
if (type == DataScopeType.ALL) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
boolean isSuccess = false;
|
||||
for (DataColumn dataColumn : dataPermission.value()) {
|
||||
// 包含权限标识符 这直接跳过
|
||||
if (ignoreMap.containsKey(dataColumn)) {
|
||||
// 修复多角色与权限标识符共用问题 https://gitee.com/dromara/RuoYi-Vue-Plus/issues/IB4CS4
|
||||
conditions.add(joinStr + " 1 = 1 ");
|
||||
isSuccess = true;
|
||||
continue;
|
||||
}
|
||||
// 不包含 key 变量 则不处理
|
||||
if (!StringUtils.containsAny(type.getSqlTemplate(), keys.toArray(String[]::new))) {
|
||||
continue;
|
||||
}
|
||||
// 当前注解不满足模板 不处理
|
||||
if (!StringUtils.containsAny(type.getSqlTemplate(), dataColumn.key())) {
|
||||
continue;
|
||||
}
|
||||
// 忽略数据权限 防止spel表达式内有其他sql查询导致死循环调用
|
||||
String sql = DataPermissionHelper.ignore(() ->
|
||||
parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class)
|
||||
);
|
||||
// 解析sql模板并填充
|
||||
conditions.add(joinStr + sql);
|
||||
isSuccess = true;
|
||||
}
|
||||
// 未处理成功则填充兜底方案
|
||||
if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
|
||||
conditions.add(joinStr + type.getElseSql());
|
||||
}
|
||||
}
|
||||
|
||||
if (CollUtil.isNotEmpty(conditions)) {
|
||||
String sql = StreamUtils.join(conditions, Function.identity(), "");
|
||||
return sql.substring(joinStr.length());
|
||||
}
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描指定包下的 Mapper 类,并查找其中带有特定注解的方法或类
|
||||
*
|
||||
* @param mapperPackage Mapper 类所在的包路径
|
||||
*/
|
||||
private void scanMapperClasses(String mapperPackage) {
|
||||
// 创建资源解析器和元数据读取工厂
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
|
||||
// 将 Mapper 包路径按分隔符拆分为数组
|
||||
String[] packagePatternArray = StringUtils.splitPreserveAllTokens(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
|
||||
String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
|
||||
try {
|
||||
for (String packagePattern : packagePatternArray) {
|
||||
// 将包路径转换为资源路径
|
||||
String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
|
||||
// 获取指定路径下的所有 .class 文件资源
|
||||
Resource[] resources = resolver.getResources(classpath + path + "/*.class");
|
||||
for (Resource resource : resources) {
|
||||
// 获取资源的类元数据
|
||||
ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
|
||||
// 获取资源对应的类对象
|
||||
Class<?> clazz = Resources.classForName(classMetadata.getClassName());
|
||||
// 查找类中的特定注解
|
||||
findAnnotation(clazz);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("初始化数据安全缓存时出错:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定的类中查找特定的注解 DataPermission,并将带有这个注解的方法或类存储到 dataPermissionCacheMap 中
|
||||
*
|
||||
* @param clazz 要查找的类
|
||||
*/
|
||||
private void findAnnotation(Class<?> clazz) {
|
||||
DataPermission dataPermission;
|
||||
for (Method method : clazz.getMethods()) {
|
||||
if (method.isDefault() || method.isVarArgs()) {
|
||||
continue;
|
||||
}
|
||||
String mappedStatementId = clazz.getName() + "." + method.getName();
|
||||
if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
|
||||
dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
|
||||
dataPermissionCacheMap.put(mappedStatementId, dataPermission);
|
||||
}
|
||||
}
|
||||
if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
|
||||
dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
|
||||
dataPermissionCacheMap.put(clazz.getName(), dataPermission);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象
|
||||
*
|
||||
* @param mapperId 映射语句 ID
|
||||
* @return DataPermission 注解对象,如果不存在则返回 null
|
||||
*/
|
||||
public DataPermission getDataPermission(String mapperId) {
|
||||
// 检查上下文中是否包含映射语句 ID 对应的 DataPermission 注解对象
|
||||
if (DataPermissionHelper.getPermission() != null) {
|
||||
return DataPermissionHelper.getPermission();
|
||||
}
|
||||
// 检查缓存中是否包含映射语句 ID 对应的 DataPermission 注解对象
|
||||
if (dataPermissionCacheMap.containsKey(mapperId)) {
|
||||
return dataPermissionCacheMap.get(mapperId);
|
||||
}
|
||||
// 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象,则尝试使用类名作为键查找
|
||||
String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
|
||||
if (dataPermissionCacheMap.containsKey(clazzName)) {
|
||||
return dataPermissionCacheMap.get(clazzName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象
|
||||
*
|
||||
* @param mapperId 映射语句 ID
|
||||
* @return 如果找到对应的 DataPermission 注解对象,则返回 false;否则返回 true
|
||||
*/
|
||||
public boolean invalid(String mapperId) {
|
||||
return getDataPermission(mapperId) == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有null变量找不到的变量返回默认值
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
private static class NullSafeStandardEvaluationContext extends StandardEvaluationContext {
|
||||
|
||||
private final Object defaultValue;
|
||||
|
||||
@Override
|
||||
public Object lookupVariable(String name) {
|
||||
Object obj = super.lookupVariable(name);
|
||||
// 如果读取到的值是 null,则返回默认值
|
||||
if (obj == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有null变量找不到的变量返回默认值 委托模式 将不需要处理的方法委托给原处理器
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
private static class NullSafePropertyAccessor implements PropertyAccessor {
|
||||
|
||||
private final PropertyAccessor delegate;
|
||||
private final Object defaultValue;
|
||||
|
||||
@Override
|
||||
public Class<?>[] getSpecificTargetClasses() {
|
||||
return delegate.getSpecificTargetClasses();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
return delegate.canRead(context, target, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
TypedValue value = delegate.read(context, target, name);
|
||||
// 如果读取到的值是 null,则返回默认值
|
||||
if (value.getValue() == null) {
|
||||
return new TypedValue(defaultValue);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
return delegate.canWrite(context, target, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
|
||||
delegate.write(context, target, name, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.ruoyi.handler;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.core.utils.reflect.ReflectUtils;
|
||||
|
||||
|
||||
/**
|
||||
* 修改表信息初始化方式
|
||||
* 目前用于全局修改是否使用逻辑删除
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class PlusPostInitTableInfoHandler implements PostInitTableInfoHandler {
|
||||
|
||||
@Override
|
||||
public void postTableInfo(TableInfo tableInfo, Configuration configuration) {
|
||||
String flag = SpringUtils.getProperty("mybatis-plus.enableLogicDelete", "true");
|
||||
// 只有关闭时 统一设置false 为true时mp自动判断不处理
|
||||
if (!Convert.toBool(flag)) {
|
||||
ReflectUtils.setFieldValue(tableInfo, "withLogicDelete", false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package org.ruoyi.common.mybatis.helper;
|
||||
package org.ruoyi.helper;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.mybatis.enums.DataBaseType;
|
||||
import org.ruoyi.enums.DataBaseType;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
@@ -62,8 +63,8 @@ public class DataBaseHelper {
|
||||
// charindex(',100,' , ',0,100,101,') <> 0
|
||||
return "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2);
|
||||
} else if (dataBasyType == DataBaseType.POSTGRE_SQL) {
|
||||
// (select position(',100,' in ',0,100,101,')) <> 0
|
||||
return "(select position(',%s,' in ','||%s||',')) <> 0".formatted(var, var2);
|
||||
// (select strpos(',0,100,101,' , ',100,')) <> 0
|
||||
return "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
|
||||
} else if (dataBasyType == DataBaseType.ORACLE) {
|
||||
// instr(',0,100,101,' , ',100,') <> 0
|
||||
return "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);
|
||||
@@ -71,6 +72,7 @@ public class DataBaseHelper {
|
||||
// find_in_set(100 , '0,100,101')
|
||||
return "find_in_set('%s' , %s) <> 0".formatted(var, var2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前加载的数据库名
|
||||
*/
|
||||
@@ -0,0 +1,176 @@
|
||||
package org.ruoyi.helper;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.context.model.SaStorage;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
|
||||
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.ruoyi.annotation.DataPermission;
|
||||
import org.ruoyi.common.core.utils.reflect.ReflectUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 数据权限助手
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("unchecked cast")
|
||||
public class DataPermissionHelper {
|
||||
|
||||
private static final String DATA_PERMISSION_KEY = "data:permission";
|
||||
|
||||
private static final ThreadLocal<Stack<Integer>> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new);
|
||||
|
||||
private static final ThreadLocal<DataPermission> PERMISSION_CACHE = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 获取当前执行mapper权限注解
|
||||
*
|
||||
* @return 返回当前执行mapper权限注解
|
||||
*/
|
||||
public static DataPermission getPermission() {
|
||||
return PERMISSION_CACHE.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前执行mapper权限注解
|
||||
*
|
||||
* @param dataPermission 数据权限注解
|
||||
*/
|
||||
public static void setPermission(DataPermission dataPermission) {
|
||||
PERMISSION_CACHE.set(dataPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前执行mapper权限注解
|
||||
*/
|
||||
public static void removePermission() {
|
||||
PERMISSION_CACHE.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从上下文中获取指定键的变量值,并将其转换为指定的类型
|
||||
*
|
||||
* @param key 变量的键
|
||||
* @param <T> 变量值的类型
|
||||
* @return 指定键的变量值,如果不存在则返回 null
|
||||
*/
|
||||
public static <T> T getVariable(String key) {
|
||||
Map<String, Object> context = getContext();
|
||||
return (T) context.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上下文中设置指定键的变量值
|
||||
*
|
||||
* @param key 要设置的变量的键
|
||||
* @param value 要设置的变量值
|
||||
*/
|
||||
public static void setVariable(String key, Object value) {
|
||||
Map<String, Object> context = getContext();
|
||||
context.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据权限上下文
|
||||
*
|
||||
* @return 存储在SaStorage中的Map对象,用于存储数据权限相关的上下文信息
|
||||
* @throws NullPointerException 如果数据权限上下文类型异常,则抛出NullPointerException
|
||||
*/
|
||||
public static Map<String, Object> getContext() {
|
||||
SaStorage saStorage = SaHolder.getStorage();
|
||||
Object attribute = saStorage.get(DATA_PERMISSION_KEY);
|
||||
if (ObjectUtil.isNull(attribute)) {
|
||||
saStorage.set(DATA_PERMISSION_KEY, new HashMap<>());
|
||||
attribute = saStorage.get(DATA_PERMISSION_KEY);
|
||||
}
|
||||
if (attribute instanceof Map map) {
|
||||
return map;
|
||||
}
|
||||
throw new NullPointerException("data permission context type exception");
|
||||
}
|
||||
|
||||
private static IgnoreStrategy getIgnoreStrategy() {
|
||||
Object ignoreStrategyLocal = ReflectUtils.getStaticFieldValue(ReflectUtils.getField(InterceptorIgnoreHelper.class, "IGNORE_STRATEGY_LOCAL"));
|
||||
if (ignoreStrategyLocal instanceof ThreadLocal<?> IGNORE_STRATEGY_LOCAL) {
|
||||
if (IGNORE_STRATEGY_LOCAL.get() instanceof IgnoreStrategy ignoreStrategy) {
|
||||
return ignoreStrategy;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
|
||||
*/
|
||||
public static void enableIgnore() {
|
||||
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
|
||||
if (ObjectUtil.isNull(ignoreStrategy)) {
|
||||
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
|
||||
} else {
|
||||
ignoreStrategy.setDataPermission(true);
|
||||
}
|
||||
Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
|
||||
reentrantStack.push(reentrantStack.size() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭忽略数据权限
|
||||
*/
|
||||
public static void disableIgnore() {
|
||||
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
|
||||
if (ObjectUtil.isNotNull(ignoreStrategy)) {
|
||||
boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
|
||||
&& !Boolean.TRUE.equals(ignoreStrategy.getBlockAttack())
|
||||
&& !Boolean.TRUE.equals(ignoreStrategy.getIllegalSql())
|
||||
&& !Boolean.TRUE.equals(ignoreStrategy.getTenantLine())
|
||||
&& CollectionUtil.isEmpty(ignoreStrategy.getOthers());
|
||||
Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
|
||||
boolean empty = reentrantStack.isEmpty() || reentrantStack.pop() == 1;
|
||||
if (noOtherIgnoreStrategy && empty) {
|
||||
InterceptorIgnoreHelper.clearIgnoreStrategy();
|
||||
} else if (empty) {
|
||||
ignoreStrategy.setDataPermission(false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在忽略数据权限中执行
|
||||
*
|
||||
* @param handle 处理执行方法
|
||||
*/
|
||||
public static void ignore(Runnable handle) {
|
||||
enableIgnore();
|
||||
try {
|
||||
handle.run();
|
||||
} finally {
|
||||
disableIgnore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在忽略数据权限中执行
|
||||
*
|
||||
* @param handle 处理执行方法
|
||||
*/
|
||||
public static <T> T ignore(Supplier<T> handle) {
|
||||
enableIgnore();
|
||||
try {
|
||||
return handle.get();
|
||||
} finally {
|
||||
disableIgnore();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package org.ruoyi.interceptor;
|
||||
|
||||
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
import net.sf.jsqlparser.statement.delete.Delete;
|
||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||
import net.sf.jsqlparser.statement.select.Select;
|
||||
import net.sf.jsqlparser.statement.select.SetOperationList;
|
||||
import net.sf.jsqlparser.statement.update.Update;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlCommandType;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
import org.ruoyi.handler.PlusDataPermissionHandler;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据权限拦截器
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
|
||||
|
||||
private final PlusDataPermissionHandler dataPermissionHandler;
|
||||
|
||||
/**
|
||||
* 构造函数,初始化 PlusDataPermissionHandler 实例
|
||||
*
|
||||
* @param mapperPackage 扫描的映射器包
|
||||
*/
|
||||
public PlusDataPermissionInterceptor(String mapperPackage) {
|
||||
this.dataPermissionHandler = new PlusDataPermissionHandler(mapperPackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在执行查询之前,检查并处理数据权限相关逻辑
|
||||
*
|
||||
* @param executor MyBatis 执行器对象
|
||||
* @param ms 映射语句对象
|
||||
* @param parameter 方法参数
|
||||
* @param rowBounds 分页对象
|
||||
* @param resultHandler 结果处理器
|
||||
* @param boundSql 绑定的 SQL 对象
|
||||
* @throws SQLException 如果发生 SQL 异常
|
||||
*/
|
||||
@Override
|
||||
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
|
||||
// 检查是否需要忽略数据权限处理
|
||||
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
// 检查是否缺少有效的数据权限注解
|
||||
if (dataPermissionHandler.invalid(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
// 解析 sql 分配对应方法
|
||||
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
|
||||
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在准备 SQL 语句之前,检查并处理更新和删除操作的数据权限相关逻辑
|
||||
*
|
||||
* @param sh MyBatis StatementHandler 对象
|
||||
* @param connection 数据库连接对象
|
||||
* @param transactionTimeout 事务超时时间
|
||||
*/
|
||||
@Override
|
||||
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
|
||||
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
|
||||
MappedStatement ms = mpSh.mappedStatement();
|
||||
// 获取 SQL 命令类型(增、删、改、查)
|
||||
SqlCommandType sct = ms.getSqlCommandType();
|
||||
|
||||
// 只处理更新和删除操作的 SQL 语句
|
||||
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
|
||||
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
// 检查是否缺少有效的数据权限注解
|
||||
if (dataPermissionHandler.invalid(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
|
||||
mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SELECT 查询语句中的 WHERE 条件
|
||||
*
|
||||
* @param select SELECT 查询对象
|
||||
* @param index 查询语句的索引
|
||||
* @param sql 查询语句
|
||||
* @param obj WHERE 条件参数
|
||||
*/
|
||||
@Override
|
||||
protected void processSelect(Select select, int index, String sql, Object obj) {
|
||||
if (select instanceof PlainSelect) {
|
||||
this.setWhere((PlainSelect) select, (String) obj);
|
||||
} else if (select instanceof SetOperationList setOperationList) {
|
||||
List<Select> selectBodyList = setOperationList.getSelects();
|
||||
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 UPDATE 语句中的 WHERE 条件
|
||||
*
|
||||
* @param update UPDATE 查询对象
|
||||
* @param index 查询语句的索引
|
||||
* @param sql 查询语句
|
||||
* @param obj WHERE 条件参数
|
||||
*/
|
||||
@Override
|
||||
protected void processUpdate(Update update, int index, String sql, Object obj) {
|
||||
Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
|
||||
if (null != sqlSegment) {
|
||||
update.setWhere(sqlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 DELETE 语句中的 WHERE 条件
|
||||
*
|
||||
* @param delete DELETE 查询对象
|
||||
* @param index 查询语句的索引
|
||||
* @param sql 查询语句
|
||||
* @param obj WHERE 条件参数
|
||||
*/
|
||||
@Override
|
||||
protected void processDelete(Delete delete, int index, String sql, Object obj) {
|
||||
Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
|
||||
if (null != sqlSegment) {
|
||||
delete.setWhere(sqlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 SELECT 语句的 WHERE 条件
|
||||
*
|
||||
* @param plainSelect SELECT 查询对象
|
||||
* @param mappedStatementId 映射语句的 ID
|
||||
*/
|
||||
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
|
||||
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
|
||||
if (null != sqlSegment) {
|
||||
plainSelect.setWhere(sqlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建表达式,用于处理表的数据权限
|
||||
*
|
||||
* @param table 表对象
|
||||
* @param where WHERE 条件表达式
|
||||
* @param whereSegment WHERE 条件片段
|
||||
* @return 构建的表达式
|
||||
*/
|
||||
@Override
|
||||
public Expression buildTableExpression(Table table, Expression where, String whereSegment) {
|
||||
// 只有新版数据权限处理器才会执行到这里
|
||||
final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
|
||||
return handler.getSqlSegment(table, where, whereSegment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
org.ruoyi.common.mybatis.config.MybatisPlusConfig
|
||||
org.ruoyi.config.MybatisPlusConfig
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# 内置配置 不允许修改 如需修改请在 nacos 上写相同配置覆盖
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
mybatis-plus:
|
||||
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
|
||||
checkConfigLocation: false
|
||||
configuration:
|
||||
# 自动驼峰命名规则(camel case)映射
|
||||
mapUnderscoreToCamelCase: true
|
||||
# MyBatis 自动映射策略
|
||||
# NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射
|
||||
autoMappingBehavior: FULL
|
||||
# MyBatis 自动映射时未知列或未知属性处理策
|
||||
# NONE:不做处理 WARNING:打印相关警告 FAILING:抛出异常和详细信息
|
||||
autoMappingUnknownColumnBehavior: NONE
|
||||
# 更详细的日志输出 会有性能损耗 org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
# 关闭日志记录 (可单纯使用 p6spy 分析) org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||
global-config:
|
||||
# 是否打印 Logo banner
|
||||
banner: true
|
||||
dbConfig:
|
||||
# 主键类型
|
||||
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
|
||||
idType: ASSIGN_ID
|
||||
# 逻辑已删除值(可按需求随意修改)
|
||||
logicDeleteValue: 1
|
||||
# 逻辑未删除值
|
||||
logicNotDeleteValue: 0
|
||||
insertStrategy: NOT_NULL
|
||||
updateStrategy: NOT_NULL
|
||||
whereStrategy: NOT_NULL
|
||||
@@ -0,0 +1,20 @@
|
||||
# p6spy 性能分析插件配置文件
|
||||
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
|
||||
# 自定义日志打印
|
||||
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
|
||||
#日志输出到控制台
|
||||
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
|
||||
# 使用日志系统记录 sql
|
||||
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
|
||||
# 取消JDBC URL前缀
|
||||
useprefix=true
|
||||
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
|
||||
excludecategories=info,debug,result,commit,resultset
|
||||
# 日期格式
|
||||
dateformat=yyyy-MM-dd HH:mm:ss
|
||||
# SQL语句打印时间格式
|
||||
databaseDialectTimestampFormat=yyyy-MM-dd HH:mm:ss
|
||||
# 是否过滤 Log
|
||||
filter=true
|
||||
# 过滤 Log 时所排除的 sql 关键字,以逗号分隔
|
||||
exclude=
|
||||
@@ -11,38 +11,33 @@
|
||||
</parent>
|
||||
|
||||
<artifactId>ruoyi-common-pay</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>3.3.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.stripe</groupId>
|
||||
<artifactId>stripe-java</artifactId>
|
||||
<version>22.5.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.12</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<zxing.version>3.3.3</zxing.version>
|
||||
<stripe.version>22.5.1</stripe.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>${zxing.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.stripe</groupId>
|
||||
<artifactId>stripe-java</artifactId>
|
||||
<version>${stripe.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -23,7 +23,6 @@ public class PayInit {
|
||||
private PayConfig payConfig;
|
||||
|
||||
@Bean
|
||||
@Scope("singleton")
|
||||
public PayConfig payConfig() {
|
||||
if (payConfig == null) {
|
||||
payConfig = new PayConfig();
|
||||
@@ -32,7 +31,6 @@ public class PayInit {
|
||||
return payConfig;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 10000) // 每10秒检查一次
|
||||
public void updatePayConfig() {
|
||||
payConfig.setType("wxpay");
|
||||
payConfig.setDevice("pc");
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.ruoyi.common.satoken.utils;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.context.model.SaStorage;
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import cn.dev33.satoken.stp.SaLoginModel;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
@@ -73,8 +74,11 @@ public class LoginHelper {
|
||||
if (loginUser != null) {
|
||||
return loginUser;
|
||||
}
|
||||
loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
|
||||
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
|
||||
SaSession tokenSession = StpUtil.getTokenSession();
|
||||
if (tokenSession != null) {
|
||||
loginUser = (LoginUser) tokenSession.get(LOGIN_USER_KEY);
|
||||
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
|
||||
};
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
@@ -162,4 +166,16 @@ public class LoginHelper {
|
||||
return isTenantAdmin(getLoginUser().getRolePermission());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前用户是否已登录
|
||||
*
|
||||
* @return 结果
|
||||
*/
|
||||
public static boolean isLogin() {
|
||||
try {
|
||||
return getLoginUser() != null;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import org.ruoyi.common.core.utils.reflect.ReflectUtils;
|
||||
import org.ruoyi.common.mybatis.config.MybatisPlusConfig;
|
||||
|
||||
import org.ruoyi.common.redis.config.RedisConfig;
|
||||
import org.ruoyi.common.redis.config.properties.RedissonProperties;
|
||||
import org.ruoyi.common.tenant.core.TenantSaTokenDao;
|
||||
@@ -17,6 +17,7 @@ import org.ruoyi.common.tenant.properties.TenantProperties;
|
||||
import org.redisson.config.ClusterServersConfig;
|
||||
import org.redisson.config.SingleServerConfig;
|
||||
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
|
||||
import org.ruoyi.config.MybatisPlusConfig;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
@@ -2,7 +2,8 @@ package org.ruoyi.common.tenant.core;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
import org.ruoyi.core.domain.BaseEntity;
|
||||
|
||||
|
||||
/**
|
||||
* 租户基类
|
||||
|
||||
@@ -94,6 +94,27 @@ public class TenantHelper {
|
||||
SaHolder.getStorage().set(cacheKey, tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置动态租户(一直有效 需要手动清理)
|
||||
* <p>
|
||||
* 如果为未登录状态下 那么只在当前线程内生效
|
||||
*
|
||||
* @param tenantId 租户id
|
||||
* @param global 是否全局生效
|
||||
*/
|
||||
public static void setDynamic(String tenantId, boolean global) {
|
||||
if (!isEnable()) {
|
||||
return;
|
||||
}
|
||||
if (!LoginHelper.isLogin() || !global) {
|
||||
TEMP_DYNAMIC_TENANT.set(tenantId);
|
||||
return;
|
||||
}
|
||||
String cacheKey = DYNAMIC_TENANT_KEY + ":" + LoginHelper.getUserId();
|
||||
RedisUtils.setCacheObject(cacheKey, tenantId);
|
||||
SaHolder.getStorage().set(cacheKey, tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动态租户(一直有效 需要手动清理)
|
||||
* <p>
|
||||
@@ -137,4 +158,18 @@ public class TenantHelper {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在动态租户中执行
|
||||
*
|
||||
* @param handle 处理执行方法
|
||||
*/
|
||||
public static void dynamic(String tenantId, Runnable handle) {
|
||||
setDynamic(tenantId);
|
||||
try {
|
||||
handle.run();
|
||||
} finally {
|
||||
clearDynamic();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,20 +36,6 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
|
||||
String url = request.getMethod() + " " + request.getRequestURI();
|
||||
String domainName = request.getServerName();
|
||||
log.info("域名信息:{}",domainName);
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
List<String> urls = whitelistUrls();
|
||||
boolean isWhitelisted = urls.stream().anyMatch(requestURI::startsWith);
|
||||
|
||||
if (!isWhitelisted){
|
||||
// 根据授权编号查询激活状态
|
||||
// ConfigService configService = SpringUtils.context().getBean(ConfigService.class);
|
||||
// String authNo = configService.getConfigValue("sys", "authcode");
|
||||
// if(!configService.checkAuth(authNo,domainName)){
|
||||
// throw new BaseException("系统未激活,请联系管理员授权");
|
||||
// }
|
||||
}
|
||||
|
||||
// 打印请求参数
|
||||
if (isJsonRequest(request)) {
|
||||
String jsonParam = "";
|
||||
@@ -67,7 +53,6 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
|
||||
log.debug("[PLUS]开始请求 => URL[{}],无参数", url);
|
||||
}
|
||||
}
|
||||
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
invokeTimeTL.set(stopWatch);
|
||||
stopWatch.start();
|
||||
@@ -99,15 +84,4 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 授权白名单
|
||||
public List<String> whitelistUrls() {
|
||||
return Arrays.asList(
|
||||
"/chat/config",
|
||||
"/pay",
|
||||
"/weixin",
|
||||
"/user/qrcode",
|
||||
"/user/login/qrcode"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
<version>${revision}</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>ruoyi-common-wechat</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<description>ruoyi-common-wechat 微信服务</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
@@ -21,6 +23,7 @@
|
||||
<artifactId>jfinal</artifactId>
|
||||
<version>3.5</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.jfinal</groupId>
|
||||
<artifactId>cos</artifactId>
|
||||
@@ -44,16 +47,19 @@
|
||||
<artifactId>emoji-java</artifactId>
|
||||
<version>3.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.activation</groupId>
|
||||
<artifactId>activation</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.mamoe</groupId>
|
||||
<artifactId>mirai-core-jvm</artifactId>
|
||||
<version>2.16.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-json</artifactId>
|
||||
@@ -64,5 +70,6 @@
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>1.2.31</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.ruoyi.common.wechat.web.enums;
|
||||
import com.jfinal.plugin.activerecord.Record;
|
||||
|
||||
import java.util.*;
|
||||
public enum KeyMsgValueType {
|
||||
public enum KeyMsgValueType {
|
||||
|
||||
IMG("IMG", "图片"),
|
||||
FILE("FILE", "文件"),
|
||||
|
||||
20
ruoyi-extend/pom.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>ruoyi-ai</artifactId>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>ruoyi-extend</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>ruoyi-mcp-server</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
76
ruoyi-extend/ruoyi-mcp-server/pom.xml
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.4</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-mcp-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>ruoyi-mcp-serve</name>
|
||||
<description>ruoyi-mcp-serve</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<spring-ai.version>1.0.0-M7</spring-ai.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.ruoyi.mcpserve;
|
||||
|
||||
import org.ruoyi.mcpserve.service.ToolService;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* @author ageer
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class RuoyiMcpServeApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(RuoyiMcpServeApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider systemTools(ToolService toolService) {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(toolService).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.ruoyi.mcpserve.service;
|
||||
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
/**
|
||||
* @author ageer
|
||||
*/
|
||||
@Service
|
||||
public class ToolService {
|
||||
|
||||
@Tool(description = "获取一个指定前缀的随机数")
|
||||
public String add(@ToolParam(description = "字符前缀") String prefix) {
|
||||
// 定义日期格式
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMdd");
|
||||
//根据当前时间获取yyMMdd格式的时间字符串
|
||||
String format = LocalDate.now().format(formatter);
|
||||
//生成随机数
|
||||
String replace = prefix + UUID.randomUUID().toString().replace("-", "");
|
||||
return format + replace;
|
||||
}
|
||||
|
||||
@Tool(description = "获取当前时间")
|
||||
public LocalDateTime getCurrentTime() {
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
server:
|
||||
port: 8081
|
||||
spring:
|
||||
ai:
|
||||
mcp:
|
||||
server:
|
||||
name: ruoyi-mcp-serve
|
||||
version: 1.0.0
|
||||
|
||||
|
||||