Compare commits

..

41 Commits

Author SHA1 Message Date
Cursor Agent
79a815b6f3 Optimize BpmTaskServiceImpl with improved task retrieval and filtering logic
Co-authored-by: dingjiangying <dingjiangying@meituan.com>
2025-07-16 11:44:44 +00:00
YunaiV
3f9c3661a8 【同步】jdk21 和 jdk8 的代码 2025-07-15 22:39:20 +08:00
YunaiV
adece67532 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro
# Conflicts:
#	yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java
#	yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java
2025-07-15 22:37:27 +08:00
YunaiV
ea5b12f21e fix:【AI 大模型】RedisVectorStore 支持 username、password 2025-07-14 22:44:50 +08:00
YunaiV
39ecf5ebe5 feat:【AI 大模型】文心一言存在 springai 接入问题,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 2025-07-14 22:42:56 +08:00
YunaiV
750709d706 feat:【AI 大模型】deepseek、azure、baichuan、moonshot 适配 1.0.0 2025-07-14 21:40:00 +08:00
YunaiV
3d0eb77148 feat:【AI 大模型】引入 spring-ai-starter-model-deepseek 依赖 2025-07-14 20:56:00 +08:00
YunaiV
c789418a7b feat:【AI 大模型】依赖 spring ai 升级到 1.0.0 2025-07-14 20:34:42 +08:00
YunaiV
e50250449a fix:【AI 大模型】UserProfileQueryToolFunction 没有参数,会报错 2025-07-14 19:50:02 +08:00
YunaiV
c96f6bb360 fix:【CRM 客户管理】CRM 超管,无法强制转移数据的问题 2025-07-14 13:19:14 +08:00
YunaiV
af94536a06 fix:【CRM 客户管理】修改数据时,避免操作日志出现“删除负责人”的情况 2025-07-14 13:10:51 +08:00
YunaiV
a9c7b584cc fix:【MALL 商城管理】优惠劵扣减时,增加 WHERE 乐观锁 2025-07-13 17:06:40 +08:00
YunaiV
a54e743a88 feat:【SYSTEM 系统管理】手机验证码登录,增加限流 2025-07-13 16:15:18 +08:00
YunaiV
64516b2210 fix:【INFRA 基础设施】文件上传时,directory 支持任意路径的问题 2025-07-13 16:06:41 +08:00
YunaiV
e7c9e3dc23 fix:【INFRA 基础设施】更新 pgsql quartz sql 2025-07-13 11:16:14 +08:00
YunaiV
7a8c37d7ac feat:【REPORT 报表】jimureport from 2.0.0 to 2.1.0 2025-07-11 23:47:15 +08:00
YunaiV
432609b6fa Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro
# Conflicts:
#	yudao-dependencies/pom.xml
2025-07-11 23:43:53 +08:00
YunaiV
520fb79e2a feat:【REPORT 报表】jimureport from 1.9.4 to 2.0.0 2025-07-11 23:42:52 +08:00
YunaiV
9b860d9ae5 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro 2025-07-11 22:06:11 +08:00
YunaiV
285da13989 fix:【SYSTEM 系统管理】修复 DefaultDBFieldHandler 在 Async 情况下,无法获取到 getLoginUserId 的问题 2025-07-11 22:04:13 +08:00
YunaiV
b1d439abf8 fix:【SYSTEM 系统管理】增加 lombok-mapstruct-binding 依赖,解决 IDEA 偶然出现 No property named “xxx" exists 的编译错误 2025-07-11 21:57:56 +08:00
YunaiV
b81b8e7aff Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro 2025-07-11 21:57:16 +08:00
YunaiV
7a5e28d08a fix:【SYSTEM 系统管理】增加 lombok-mapstruct-binding 依赖,解决 IDEA 偶然出现 No property named “xxx" exists 的编译错误 2025-07-11 21:57:09 +08:00
YunaiV
4d5e501a41 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro
# Conflicts:
#	yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
#	yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java
2025-07-11 21:54:11 +08:00
YunaiV
876659b01d chore:spring security from 5.8.14 to 5.8.16 2025-07-10 09:08:32 +08:00
YunaiV
569ff42e6f feat:【SYSTEM 系统管理】优化支持 SimpleAsyncTaskExecutor 异步线程池 2025-07-09 23:24:17 +08:00
YunaiV
bda357508a feat:【SYSTEM 系统管理】SecurityFrameworkUtils 的 setLoginUser 方法,移除对 request 强依赖,可用于 Job 记录操作日志; 2025-07-09 22:37:48 +08:00
YunaiV
7fc522938d fix:【SYSTEM 系统管理】社交绑定,使用 code 授权码的参数错误 2025-07-09 19:27:07 +08:00
YunaiV
563985dcfc feat:【MP 公众号】兼容无 API 权限的个人公众号场景 2025-07-09 19:17:50 +08:00
YunaiV
d8e1610495 feat: enhance MyBatis utility for sorting and update SQL aliases in TradeOrderStatisticsMapper 2025-07-08 09:39:44 +08:00
YunaiV
eca9307344 fix:【MALL 商城】修复退款通知中 merchantRefundId 字符串转换 Long 类型的问题 2025-07-06 16:38:14 +08:00
YunaiV
a690184524 feat:【ERP 系统】清理多余的 ErpStatisticsServiceTest 类 2025-07-06 16:19:45 +08:00
YunaiV
2ee1e15101 Merge branch 'master-jdk17' of https://github.com/YunaiV/ruoyi-vue-pro into master-jdk17 2025-07-06 16:08:37 +08:00
芋道源码
e6fecd8efe Merge pull request #869 from YunaiV/fix-erp-statistics-tenant-issue
fix: ERP统计查询在多租户关闭时的NullPointerException问题
2025-07-06 16:08:21 +08:00
芋道源码
94e280eb34 !1371 fix: 修复上传MP4文件之后通过下载连接下载MP4文件源文件能通过windows自带播放器播放下载下来的播放不了
Merge pull request !1371 from puhui999/master-jdk17
2025-06-22 06:48:35 +00:00
芋道源码
ec5281f2e5 !1373 fix: 创建拼团活动时校验商品是否参与了其它活动
Merge pull request !1373 from puhui999/master-jdk17-2
2025-06-22 06:46:30 +00:00
芋道源码
6ac8fa28a7 !1374 fix: 修复拼团还没成团,虚拟成团是否,这个时候核销也能核销成功
Merge pull request !1374 from puhui999/master-jdk17-3
2025-06-22 06:44:49 +00:00
puhui999
cac82a13a7 fix: 修复拼团还没成团,虚拟成团是否,这个时候核销也能核销成功 2025-06-18 17:23:59 +08:00
puhui999
9fc4a4061f fix: 创建拼团活动时校验商品是否参与了其它活动 2025-06-18 15:40:51 +08:00
puhui999
a5ad8bb708 fix: 修复上传MP4文件之后通过下载连接下载MP4文件源文件能通过windows自带播放器播放下载下来的播放不了 2025-06-18 12:45:22 +08:00
芋道源码
3b2a3dd0ea fix: ERP统计查询在多租户关闭时的NullPointerException问题
- 修复ErpSaleStatisticsMapper.xml中硬编码使用getRequiredTenantId()导致的空指针异常
- 修复ErpPurchaseStatisticsMapper.xml中硬编码使用getRequiredTenantId()导致的空指针异常
- 使用条件判断getTenantId() != null来决定是否添加租户条件
- 添加单元测试验证多租户开启和关闭时的统计查询功能
- 确保向后兼容,多租户开启时正常工作,关闭时不报错
2025-06-09 06:36:11 +00:00
60 changed files with 1033 additions and 850 deletions

521
README.md
View File

@@ -1,244 +1,391 @@
# 🚀 芋道 ruoyi-vue-pro <p align="center">
<img src="https://img.shields.io/badge/Spring%20Boot-2.7.18-blue.svg" alt="Downloads">
<img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads">
<img src="https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro"/>
</p>
<div align="center"> **严肃声明:现在、未来都不会有商业版本,所有代码全部开源!**
![Spring Boot](https://img.shields.io/badge/Spring%20Boot-2.7.18-brightgreen.svg) **「我喜欢写代码,乐此不疲」**
![Vue](https://img.shields.io/badge/Vue-3.2-brightgreen.svg) **「我喜欢做开源,以此为乐」**
![License](https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro.svg)
![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social)
**基于 Spring Boot + Vue 的企业级快速开发平台** 我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。
**🔥 100% 开源免费,个人与企业可免费使用** 如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。
[在线演示](http://dashboard-vue3.yudao.iocoder.cn) | [快速开始](https://doc.iocoder.cn/quick-start/) | [开发文档](https://doc.iocoder.cn/) | [视频教程](https://doc.iocoder.cn/video/) ## 🐶 新手必读
</div> * 演示地址【Vue3 + element-plus】<http://dashboard-vue3.yudao.iocoder.cn>
* 演示地址【Vue3 + vben(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
* 演示地址【Vue2 + element-ui】<http://dashboard.yudao.iocoder.cn>
* 启动文档:<https://doc.iocoder.cn/quick-start/>
* 视频教程:<https://doc.iocoder.cn/video/>
## ✨ 特性 ## 🐰 版本说明
- 🎯 **开箱即用** - 基于最新技术栈,提供完整的前后端解决方案 | 版本 | JDK 8 + Spring Boot 2.7 | JDK 17/21 + Spring Boot 3.2 |
- 🔐 **权限管理** - 完善的权限认证体系,支持多租户、多终端 |---------------------------------------------------------------------|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------|
- 🔄 **工作流程** - 集成 Flowable支持可视化流程设计 | 【完整版】[ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [`master`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master-jdk17/) 分支 |
- 🛒 **业务模块** - 内置 CRM、ERP、商城、会员等业务系统 | 【精简版】[yudao-boot-mini](https://gitee.com/yudaocode/yudao-boot-mini) | [`master`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-boot-mini/tree/master-jdk17/) 分支 |
- 🎨 **多端支持** - 支持 PC、移动端、小程序
- 🚀 **高性能** - Redis 缓存、MySQL 读写分离、分布式架构
- 📊 **数据报表** - 支持报表设计器、大屏设计器
- 🤖 **AI 集成** - 支持主流 AI 大模型接入
## 🎯 在线演示 * 【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
* 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
| 版本 | 地址 | 账号 | 可参考 [《迁移文档》](https://doc.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
|------|------|------|
| Vue3 + Element Plus | [dashboard-vue3.yudao.iocoder.cn](http://dashboard-vue3.yudao.iocoder.cn) | admin/admin123 |
| Vue3 + Ant Design | [dashboard-vben.yudao.iocoder.cn](http://dashboard-vben.yudao.iocoder.cn) | admin/admin123 |
| Vue2 + Element UI | [dashboard.yudao.iocoder.cn](http://dashboard.yudao.iocoder.cn) | admin/admin123 |
## 🏗️ 项目结构 ## 🐯 平台简介
``` **芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
ruoyi-vue-pro/
├── yudao-dependencies/ # Maven 依赖管理
├── yudao-framework/ # 框架核心
├── yudao-server/ # 服务启动器
├── yudao-module-system/ # 系统管理模块
├── yudao-module-infra/ # 基础设施模块
├── yudao-module-bpm/ # 工作流程模块
├── yudao-module-pay/ # 支付系统模块
├── yudao-module-mall/ # 商城系统模块
├── yudao-module-crm/ # CRM 客户管理模块
├── yudao-module-erp/ # ERP 企业资源计划模块
├── yudao-module-ai/ # AI 大模型模块
├── yudao-module-member/ # 会员中心模块
├── yudao-module-mp/ # 微信公众号模块
├── yudao-module-report/ # 数据报表模块
└── yudao-ui/ # 前端项目
```
## 🛠️ 技术栈 > 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。
>
> 😜 给项目点点 Star 吧,这对我们真的很重要!
### 后端技术 ![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
- **核心框架**: Spring Boot 2.7.18
- **安全框架**: Spring Security + JWT
- **持久层**: MyBatis Plus + Druid
- **数据库**: MySQL 5.7/8.0+
- **缓存**: Redis + Redisson
- **工作流**: Flowable
- **任务调度**: Quartz
- **接口文档**: Swagger3 + Knife4j
### 前端技术 * Java 后端:`master` 分支为 JDK 8 + Spring Boot 2.7`master-jdk17` 分支为 JDK 17/21 + Spring Boot 3.2
- **框架**: Vue 3.x / Vue 2.x * 管理后台的电脑端Vue3 提供 `element-plus``vben(ant-design-vue)` 两个版本Vue2 提供 `element-ui` 版本
- **UI 组件**: Element Plus / Ant Design Vue / Element UI * 管理后台的移动端:采用 `uni-app` 方案,一份代码多终端适配,同时支持 APP、小程序、H5
- **构建工具**: Vite / Webpack * 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson
- **路由**: Vue Router * 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等
- **状态管理**: Pinia / Vuex * 消息队列可使用 Event、Redis、RabbitMQ、Kafka、RocketMQ 等
- **HTTP 客户端**: Axios * 权限认证使用 Spring Security & Token & Redis支持多终端、多种用户的认证系统支持 SSO 单点登录
* 支持加载动态权限菜单按钮级别权限控制Redis 缓存提升性能
* 支持 SaaS 多租户,可自定义每个租户的权限,提供透明化的多租户底层封装
* 工作流使用 Flowable支持动态表单、在线设计流程、会签 / 或签、多种任务分配方式
* 高效率开发,使用代码生成器可以一键生成 Java、Vue 前后端代码、SQL 脚本、接口文档,支持单表、树表、主子表
* 实时通信,采用 Spring WebSocket 实现,内置 Token 身份校验,支持 WebSocket 集群
* 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款
* 集成阿里云、腾讯云等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务
* 集成报表设计器、大屏设计器,通过拖拽即可生成酷炫的报表与大屏
### 开发工具 ## 🐳 项目关系
- **IDE**: IntelliJ IDEA / VS Code
- **版本控制**: Git
- **项目管理**: Maven
- **代码规范**: ESLint + Prettier
- **API 测试**: Postman / ApiPost
## 🚀 快速开始 ![架构演进](/.image/common/yudao-roadmap.png)
### 环境要求 三个项目的功能对比,可见社区共同整理的 [国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn) 表格。
- JDK 8 或 JDK 17/21 ### 后端项目
- MySQL 5.7+
- Redis 3.0+
- Maven 3.6+
- Node.js 16+
### 后端启动 | 项目 | Star | 简介 |
|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| [ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro) | 基于 Spring Boot 多模块架构 |
| [yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [![Gitee star](https://gitee.com/zhijiantianya/yudao-cloud/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/yudao-cloud) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/yudao-cloud.svg?style=social&label=Stars)](https://github.com/YunaiV/yudao-cloud) | 基于 Spring Cloud 微服务架构 |
| [Spring-Boot-Labs](https://gitee.com/yudaocode/SpringBoot-Labs) | [![Gitee star](https://gitee.com/yudaocode/SpringBoot-Labs/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/yudao-cloud) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/SpringBoot-Labs.svg?style=social&label=Stars)](https://github.com/yudaocode/SpringBoot-Labs) | 系统学习 Spring Boot & Cloud 专栏 |
1. **克隆项目** ### 前端项目
```bash
git clone https://github.com/YunaiV/ruoyi-vue-pro.git
cd ruoyi-vue-pro
```
2. **创建数据库** | 项目 | Star | 简介 |
```bash |----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------|
# 创建数据库 | [yudao-ui-admin-vue3](https://gitee.com/yudaocode/yudao-ui-admin-vue3) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue3/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue3) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue3.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue3) | 基于 Vue3 + element-plus 实现的管理后台 |
mysql -u root -p | [yudao-ui-admin-vben](https://gitee.com/yudaocode/yudao-ui-admin-vben) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vben/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vben) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vben.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vben) | 基于 Vue3 + vben(ant-design-vue) 实现的管理后台 |
CREATE DATABASE ruoyi_vue_pro DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; | [yudao-mall-uniapp](https://gitee.com/yudaocode/yudao-mall-uniapp) | [![Gitee star](https://gitee.com/yudaocode/yudao-mall-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-mall-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-mall-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-mall-uniapp) | 基于 uni-app 实现的商城小程序 |
| [yudao-ui-admin-vue2](https://gitee.com/yudaocode/yudao-ui-admin-vue2) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue2/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue2) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue2.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue2) | 基于 Vue2 + element-ui 实现的管理后台 |
| [yudao-ui-admin-uniapp](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-uniapp) | 基于 Vue2 + element-ui 实现的管理后台 |
| [yudao-ui-go-view](https://gitee.com/yudaocode/yudao-ui-go-view) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-go-view/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-go-view) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-go-view.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-go-view) | 基于 Vue3 + naive-ui 实现的大屏报表 |
# 导入初始化脚本 ## 😎 开源协议
source sql/mysql/ruoyi-vue-pro.sql
source sql/mysql/quartz.sql
```
3. **修改配置** **为什么推荐使用本项目?**
```yaml
# application-local.yaml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/ruoyi_vue_pro
username: root
password: 123456
data:
redis:
host: 127.0.0.1
port: 6379
password: # Redis 密码,没有可不填
```
4. **启动项目** ① 本项目采用比 Apache 2.0 更宽松的 [MIT License](https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/LICENSE) 开源协议,个人与企业可 100% 免费使用不用保留类作者、Copyright 信息。
```bash
mvn clean install
cd yudao-server
mvn spring-boot:run
```
### 前端启动 ② 代码全部开源,不会像其他项目一样,只开源部分代码,让你无法了解整个项目的架构设计。[国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn)
```bash ![开源项目对比](/.image/common/project-vs.png)
# 进入前端目录(以 Vue3 版本为例)
cd yudao-ui/yudao-ui-admin-vue3
# 安装依赖 ③ 代码整洁、架构整洁,遵循《阿里巴巴 Java 开发手册》规范代码注释详细113770 行 Java 代码42462 行代码注释。
npm install
# 启动开发服务器 ## 🤝 项目外包
npm run dev
# 访问地址http://localhost:80 我们也是接外包滴,如果你有项目想要外包,可以微信联系【**Aix9975**】。
```
## 📚 核心功能 团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。
### 🔐 系统管理 项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 聊天、微信公众号、微信小程序等等。
- **用户管理**: 用户信息维护、角色分配
- **角色管理**: 角色权限分配、数据权限设置
- **菜单管理**: 菜单配置、按钮权限控制
- **部门管理**: 组织架构管理、数据权限划分
- **租户管理**: SaaS 多租户支持
- **字典管理**: 系统字典维护
- **操作日志**: 系统操作记录追踪
### 📊 工作流程 ## 🐼 内置功能
- **流程设计**: 可视化流程设计器
- **表单设计**: 动态表单构建器
- **流程实例**: 流程发起、审批、监控
- **任务处理**: 待办事项、已办查询
- **流程监控**: 流程实例监控、统计分析
### 💰 支付系统 系统内置多种多种业务功能,可以用于快速你的业务系统
- **支付渠道**: 支付宝、微信支付接入
- **支付订单**: 支付订单管理、退款处理
- **商户管理**: 商户信息、应用配置
- **对账管理**: 交易对账、财务报表
### 🛍️ 商城系统 ![功能分层](/.image/common/ruoyi-vue-pro-biz.png)
- **商品管理**: 商品信息、分类、品牌管理
- **订单管理**: 订单处理、物流跟踪
- **促销活动**: 优惠券、拼团、秒杀
- **会员系统**: 会员等级、积分、成长值
### 🏢 CRM 系统 * 通用模块(必选):系统功能、基础设施
- **客户管理**: 客户信息、跟进记录 * 通用模块(可选):工作流程、支付系统、数据报表、会员中心
- **商机管理**: 销售机会、转化追踪 * 业务系统按需ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
- **合同管理**: 合同签署、执行监控
- **回款管理**: 回款计划、到账记录
### 📦 ERP 系统 > 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
- **采购管理**: 采购订单、供应商管理 >
- **销售管理**: 销售订单、客户管理 > * 额外新增的功能,我们使用 🚀 标记。
- **库存管理**: 出入库、库存盘点 > * 重新实现的功能,我们使用 ⭐️ 标记。
- **财务管理**: 应收应付、财务报表
## 🌟 开源协议 🙂 所有功能,都通过 **单元测试** 保证高质量。
本项目基于 [MIT License](./LICENSE) 开源协议,您可以: ### 系统功能
**商业使用** - 可用于商业项目 | | 功能 | 描述 |
**自由修改** - 可自由修改代码 |-----|-------|---------------------------------|
**自由分发** - 可自由分享给他人 | | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
**私用** - 可用于个人项目 | ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 |
| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 |
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
| | 岗位管理 | 配置系统用户所属担任职务 |
| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 |
| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 |
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 |
| 🚀 | 邮件管理 | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台 |
| 🚀 | 站内信 | 系统内的消息通知,提供站内信模版、站内信消息 |
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 |
| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 |
| | 通知公告 | 系统通知公告信息发布维护 |
| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 |
| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 |
## 🤝 参与贡献 ![功能图](/.image/common/system-feature.png)
欢迎各种形式的贡献,包括但不限于: ### 工作流程
- 🐛 报告 Bug ![功能图](/.image/common/bpm-feature.png)
- 💡 提出新功能建议
- 📝 完善文档
- 🔧 提交代码
### 贡献流程 基于 Flowable 构建,可支持信创(国产)数据库,满足中国特色流程操作:
1. Fork 本项目 | BPMN 设计器 | 钉钉/飞书设计器 |
2. 创建特性分支: `git checkout -b feature/AmazingFeature` |------------------------------|--------------------------------|
3. 提交更改: `git commit -m 'Add some AmazingFeature'` | ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
4. 推送到分支: `git push origin feature/AmazingFeature`
5. 提交 Pull Request
## 📞 联系我们 > 历经头部企业生产验证,工作流引擎须标配仿钉钉/飞书 + BPMN 双设计器!!!
>
> 前者支持轻量配置简单流程,后者实现复杂场景深度编排
- 📧 **邮箱**: yunai@iocoder.cn | 功能列表 | 功能描述 | 是否完成 |
- 💬 **微信**: Aix9975项目外包合作 |------------|-------------------------------------------------------------------------------------|------|
- 🌐 **官网**: [https://www.iocoder.cn](https://www.iocoder.cn) | SIMPLE 设计器 | 仿钉钉/飞书设计器支持拖拽搭建表单流程10 分钟快速完成审批流程配置 | ✅ |
- 📖 **文档**: [https://doc.iocoder.cn](https://doc.iocoder.cn) | BPMN 设计器 | 基于 BPMN 标准开发,适配复杂业务场景,满足多层级审批及流程自动化需求 | ✅ |
| 会签 | 同一个审批节点设置多个人(如 A、B、C 三人,三人会同时收到待办任务),需全部同意之后,审批才可到下一审批节点 | ✅ |
| 或签 | 同一个审批节点设置多个人,任意一个人处理后,就能进入下一个节点 | ✅ |
| 依次审批 | (顺序会签)同一个审批节点设置多个人(如 A、B、C 三人),三人按顺序依次收到待办,即 A 先审批A 提交后 B 才能审批,需全部同意之后,审批才可到下一审批节点 | ✅ |
| 抄送 | 将审批结果通知给抄送人,同一个审批默认排重,不重复抄送给同一人 | ✅ |
| 驳回 | (退回)将审批重置发送给某节点,重新审批。可驳回至发起人、上一节点、任意节点 | ✅ |
| 转办 | A 转给其 B 审批B 审批后,进入下一节点 | ✅ |
| 委派 | A 转给其 B 审批B 审批后,转给 AA 继续审批后进入下一节点 | ✅ |
| 加签 | 允许当前审批人根据需要,自行增加当前节点的审批人,支持向前、向后加签 | ✅ |
| 减签 | (取消加签)在当前审批人操作之前,减少审批人 | ✅ |
| 撤销 | (取消流程)流程发起人,可以对流程进行撤销处理 | ✅ |
| 终止 | 系统管理员,在任意节点终止流程实例 | ✅ |
| 表单权限 | 支持拖拉拽配置表单,每个审批节点可配置只读、编辑、隐藏权限 | ✅ |
| 超时审批 | 配置超时审批时间,超时后自动触发审批通过、不通过、驳回等操作 | ✅ |
| 自动提醒 | 配置提醒时间,到达时间后自动触发短信、邮箱、站内信等通知提醒,支持自定义重复提醒频次 | ✅ |
| 父子流程 | 主流程设置子流程节点,子流程节点会自动触发子流程。子流程结束后,主流程才会执行(继续往下下执行),支持同步子流程、异步子流程 | ✅ |
| 条件分支 | (排它分支)用于在流程中实现决策,即根据条件选择一个分支执行 | ✅ |
| 并行分支 | 允许将流程分成多条分支,不进行条件判断,所有分支都会执行 | ✅ |
| 包容分支 | (条件分支 + 并行分支的结合体)允许基于条件选择多条分支执行,但如果没有任何一个分支满足条件,则可以选择默认分支 | ✅ |
| 路由分支 | 根据条件选择一个分支执行(重定向到指定配置节点),也可以选择默认分支执行(继续往下执行) | ✅ |
| 触发节点 | 执行到该节点,触发 HTTP 请求、HTTP 回调、更新数据、删除数据等 | ✅ |
| 延迟节点 | 执行到该节点,审批等待一段时间再执行,支持固定时长、固定日期等 | ✅ |
| 拓展设置 | 流程前置/后置通知,节点(任务)前置、后置通知,流程报表,自动审批去重,自定流程编号、标题、摘要,流程报表等 | ✅ |
## ⭐ Star History ### 支付系统
如果这个项目对您有帮助,请给我们一个 ⭐️ Star您的支持是我们前进的动力。 | | 功能 | 描述 |
|-----|------|---------------------------|
| 🚀 | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 |
| 🚀 | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单 |
| 🚀 | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单 |
| 🚀 | 回调通知 | 查看支付回调业务的【支付】【退款】的通知结果 |
| 🚀 | 接入示例 | 提供接入支付系统的【支付】【退款】的功能实战 |
[![Star History Chart](https://api.star-history.com/svg?repos=YunaiV/ruoyi-vue-pro&type=Date)](https://star-history.com/#YunaiV/ruoyi-vue-pro&Date) ### 基础设施
## 📄 许可证 | | 功能 | 描述 |
|-----|-----------|----------------------------------------------|
| 🚀 | 代码生成 | 前后端代码的生成Java、Vue、SQL、单元测试支持 CRUD 下载 |
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
| 🚀 | 文件服务 | 支持将文件存储到 S3MinIO、阿里云、腾讯云、七牛云、本地、FTP、数据库等 |
| 🚀 | WebSocket | 提供 WebSocket 接入示例,支持一对一、一对多发送方式 |
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
| | MySQL 监控 | 监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈 |
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
| 🚀 | 消息队列 | 基于 Redis 实现消息队列Stream 提供集群消费Pub/Sub 提供广播消费 |
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
| 🚀 | 服务保障 | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景 |
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
本项目采用 MIT 许可证。详情请参见 [LICENSE](./LICENSE) 文件。 ![功能图](/.image/common/infra-feature.png)
--- ### 数据报表
<div align="center"> | | 功能 | 描述 |
|-----|-------|--------------------|
| 🚀 | 报表设计器 | 支持数据报表、图形报表、打印设计等 |
| 🚀 | 大屏设计器 | 拖拽生成数据大屏,内置几十种图表组件 |
**让开发更简单,让创业更轻松** 🚀 ### 微信公众号
Made with ❤️ by [芋道源码](https://www.iocoder.cn) | | 功能 | 描述 |
|-----|--------|-------------------------------|
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
</div> ### 商城系统
演示地址:<https://doc.iocoder.cn/mall-preview/>
![功能图](/.image/common/mall-feature.png)
![功能图](/.image/common/mall-preview.png)
### 会员中心
| | 功能 | 描述 |
|-----|------|----------------------------------|
| 🚀 | 会员管理 | 会员是 C 端的消费者,该功能用于会员的搜索与管理 |
| 🚀 | 会员标签 | 对会员的标签进行创建、查询、修改、删除等操作 |
| 🚀 | 会员等级 | 对会员的等级、成长值进行管理,可用于订单折扣等会员权益 |
| 🚀 | 会员分组 | 对会员进行分组,用于用户画像、内容推送等运营手段 |
| 🚀 | 积分签到 | 回馈给签到、消费等行为的积分,会员可订单抵现、积分兑换等途径消耗 |
### ERP 系统
演示地址:<https://doc.iocoder.cn/erp-preview/>
![功能图](/.image/common/erp-feature.png)
### CRM 系统
演示地址:<https://doc.iocoder.cn/crm-preview/>
![功能图](/.image/common/crm-feature.png)
### AI 大模型
演示地址:<https://doc.iocoder.cn/ai-preview/>
![功能图](/.image/common/ai-feature.png)
![功能图](/.image/common/ai-preview.gif)
## 🐨 技术栈
### 模块
| 项目 | 说明 |
|-----------------------|--------------------|
| `yudao-dependencies` | Maven 依赖版本管理 |
| `yudao-framework` | Java 框架拓展 |
| `yudao-server` | 管理后台 + 用户 APP 的服务端 |
| `yudao-module-system` | 系统功能的 Module 模块 |
| `yudao-module-member` | 会员中心的 Module 模块 |
| `yudao-module-infra` | 基础设施的 Module 模块 |
| `yudao-module-bpm` | 工作流程的 Module 模块 |
| `yudao-module-pay` | 支付系统的 Module 模块 |
| `yudao-module-mall` | 商城系统的 Module 模块 |
| `yudao-module-erp` | ERP 系统的 Module 模块 |
| `yudao-module-crm` | CRM 系统的 Module 模块 |
| `yudao-module-ai` | AI 大模型的 Module 模块 |
| `yudao-module-mp` | 微信公众号的 Module 模块 |
| `yudao-module-report` | 大屏报表 Module 模块 |
### 框架
| 框架 | 说明 | 版本 | 学习指南 |
|---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------|
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.7.18 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | |
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.23 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.7 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.6.1 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 / 6.0 /7.0 | |
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.32.0 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.24 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.7.11 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.5 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.8.0 | [文档](https://doc.iocoder.cn/bpm/) |
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
| [Springdoc](https://springdoc.org/) | Swagger 文档 | 1.7.0 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.12.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.7.10 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.5 | |
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.6.3 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.34 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - |
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.8.0 | - |
## 🐷 演示图
### 系统功能
| 模块 | biu | biu | biu |
|----------|-----------------------------|---------------------------|--------------------------|
| 登录 & 首页 | ![登录](/.image/登录.jpg) | ![首页](/.image/首页.jpg) | ![个人中心](/.image/个人中心.jpg) |
| 用户 & 应用 | ![用户管理](/.image/用户管理.jpg) | ![令牌管理](/.image/令牌管理.jpg) | ![应用管理](/.image/应用管理.jpg) |
| 租户 & 套餐 | ![租户管理](/.image/租户管理.jpg) | ![租户套餐](/.image/租户套餐.png) | - |
| 部门 & 岗位 | ![部门管理](/.image/部门管理.jpg) | ![岗位管理](/.image/岗位管理.jpg) | - |
| 菜单 & 角色 | ![菜单管理](/.image/菜单管理.jpg) | ![角色管理](/.image/角色管理.jpg) | - |
| 审计日志 | ![操作日志](/.image/操作日志.jpg) | ![登录日志](/.image/登录日志.jpg) | - |
| 短信 | ![短信渠道](/.image/短信渠道.jpg) | ![短信模板](/.image/短信模板.jpg) | ![短信日志](/.image/短信日志.jpg) |
| 字典 & 敏感词 | ![字典类型](/.image/字典类型.jpg) | ![字典数据](/.image/字典数据.jpg) | ![敏感词](/.image/敏感词.jpg) |
| 错误码 & 通知 | ![错误码管理](/.image/错误码管理.jpg) | ![通知公告](/.image/通知公告.jpg) | - |
### 工作流程
| 模块 | biu | biu | biu |
|---------|---------------------------------|---------------------------------|---------------------------------|
| 流程模型 | ![流程模型-列表](/.image/流程模型-列表.jpg) | ![流程模型-设计](/.image/流程模型-设计.jpg) | ![流程模型-定义](/.image/流程模型-定义.jpg) |
| 表单 & 分组 | ![流程表单](/.image/流程表单.jpg) | ![用户分组](/.image/用户分组.jpg) | - |
| 我的流程 | ![我的流程-列表](/.image/我的流程-列表.jpg) | ![我的流程-发起](/.image/我的流程-发起.jpg) | ![我的流程-详情](/.image/我的流程-详情.jpg) |
| 待办 & 已办 | ![任务列表-审批](/.image/任务列表-审批.jpg) | ![任务列表-待办](/.image/任务列表-待办.jpg) | ![任务列表-已办](/.image/任务列表-已办.jpg) |
| OA 请假 | ![OA请假-列表](/.image/OA请假-列表.jpg) | ![OA请假-发起](/.image/OA请假-发起.jpg) | ![OA请假-详情](/.image/OA请假-详情.jpg) |
### 基础设施
| 模块 | biu | biu | biu |
|---------------|-------------------------------|-----------------------------|---------------------------|
| 代码生成 | ![代码生成](/.image/代码生成.jpg) | ![生成效果](/.image/生成效果.jpg) | - |
| 文档 | ![系统接口](/.image/系统接口.jpg) | ![数据库文档](/.image/数据库文档.jpg) | - |
| 文件 & 配置 | ![文件配置](/.image/文件配置.jpg) | ![文件管理](/.image/文件管理2.jpg) | ![配置管理](/.image/配置管理.jpg) |
| 定时任务 | ![定时任务](/.image/定时任务.jpg) | ![任务日志](/.image/任务日志.jpg) | - |
| API 日志 | ![访问日志](/.image/访问日志.jpg) | ![错误日志](/.image/错误日志.jpg) | - |
| MySQL & Redis | ![MySQL](/.image/MySQL.jpg) | ![Redis](/.image/Redis.jpg) | - |
| 监控平台 | ![Java监控](/.image/Java监控.jpg) | ![链路追踪](/.image/链路追踪.jpg) | ![日志中心](/.image/日志中心.jpg) |
### 支付系统
| 模块 | biu | biu | biu |
|---------|---------------------------|---------------------------------|---------------------------------|
| 商家 & 应用 | ![商户信息](/.image/商户信息.jpg) | ![应用信息-列表](/.image/应用信息-列表.jpg) | ![应用信息-编辑](/.image/应用信息-编辑.jpg) |
| 支付 & 退款 | ![支付订单](/.image/支付订单.jpg) | ![退款订单](/.image/退款订单.jpg) | --- |
### 数据报表
| 模块 | biu | biu | biu |
|-------|---------------------------------|---------------------------------|---------------------------------------|
| 报表设计器 | ![数据报表](/.image/报表设计器-数据报表.jpg) | ![图形报表](/.image/报表设计器-图形报表.jpg) | ![报表设计器-打印设计](/.image/报表设计器-打印设计.jpg) |
| 大屏设计器 | ![大屏列表](/.image/大屏设计器-列表.jpg) | ![大屏预览](/.image/大屏设计器-预览.jpg) | ![大屏编辑](/.image/大屏设计器-编辑.jpg) |
### 移动端(管理后台)
| biu | biu | biu |
|----------------------------------|----------------------------------|----------------------------------|
| ![](/.image/admin-uniapp/01.png) | ![](/.image/admin-uniapp/02.png) | ![](/.image/admin-uniapp/03.png) |
| ![](/.image/admin-uniapp/04.png) | ![](/.image/admin-uniapp/05.png) | ![](/.image/admin-uniapp/06.png) |
| ![](/.image/admin-uniapp/07.png) | ![](/.image/admin-uniapp/08.png) | ![](/.image/admin-uniapp/09.png) |
目前已经实现登录、我的、工作台、编辑资料、头像修改、密码修改、常见问题、关于我们等基础功能。

View File

@@ -88,6 +88,13 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>${lombok.version}</version> <version>${lombok.version}</version>
</path> </path>
<path>
<!-- 确保 Lombok 生成的 getter/setter 方法能被 MapStruct 正确识别,
避免出现 No property named “xxx" exists 的编译错误 -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
<path> <path>
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId> <artifactId>mapstruct-processor</artifactId>

View File

@@ -1,253 +1,208 @@
-- ---------------------------- -- https://github.com/quartz-scheduler/quartz/blob/main/quartz/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql
-- qrtz_blob_triggers -- Thanks to Patrick Lightbody for submitting this...
-- ---------------------------- --
CREATE TABLE qrtz_blob_triggers -- In your Quartz properties file, you'll need to set
-- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
CREATE TABLE QRTZ_JOB_DETAILS
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
trigger_name varchar(190) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL,
trigger_group varchar(190) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL,
blob_data bytea NULL, DESCRIPTION VARCHAR(250) NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group) JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE BOOL NOT NULL,
IS_NONCONCURRENT BOOL NOT NULL,
IS_UPDATE_DATA BOOL NOT NULL,
REQUESTS_RECOVERY BOOL NOT NULL,
JOB_DATA BYTEA NULL,
PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
); );
CREATE INDEX idx_qrtz_blob_triggers_sched_name ON qrtz_blob_triggers (sched_name, trigger_name, trigger_group); CREATE TABLE QRTZ_TRIGGERS
-- ----------------------------
-- qrtz_calendars
-- ----------------------------
CREATE TABLE qrtz_calendars
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
calendar_name varchar(190) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL,
calendar bytea NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (sched_name, calendar_name) JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT NULL,
PREV_FIRE_TIME BIGINT NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT NOT NULL,
END_TIME BIGINT NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT NULL,
JOB_DATA BYTEA NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
);
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
REPEAT_COUNT BIGINT NOT NULL,
REPEAT_INTERVAL BIGINT NOT NULL,
TIMES_TRIGGERED BIGINT NOT NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CRON_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13, 4) NULL,
DEC_PROP_2 NUMERIC(13, 4) NULL,
BOOL_PROP_1 BOOL NULL,
BOOL_PROP_2 BOOL NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_BLOB_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BYTEA NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CALENDARS
(
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BYTEA NOT NULL,
PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
); );
-- ---------------------------- CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
-- qrtz_cron_triggers
-- ----------------------------
CREATE TABLE qrtz_cron_triggers
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
trigger_name varchar(190) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL,
trigger_group varchar(190) NOT NULL, PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
cron_expression varchar(120) NOT NULL,
time_zone_id varchar(80) NULL DEFAULT NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
); );
-- @formatter:off CREATE TABLE QRTZ_FIRED_TRIGGERS
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- qrtz_fired_triggers
-- ----------------------------
CREATE TABLE qrtz_fired_triggers
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
entry_id varchar(95) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL,
trigger_name varchar(190) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL,
trigger_group varchar(190) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL,
instance_name varchar(190) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL,
fired_time int8 NOT NULL, FIRED_TIME BIGINT NOT NULL,
sched_time int8 NOT NULL, SCHED_TIME BIGINT NOT NULL,
priority int4 NOT NULL, PRIORITY INTEGER NOT NULL,
state varchar(16) NOT NULL, STATE VARCHAR(16) NOT NULL,
job_name varchar(190) NULL DEFAULT NULL, JOB_NAME VARCHAR(200) NULL,
job_group varchar(190) NULL DEFAULT NULL, JOB_GROUP VARCHAR(200) NULL,
is_nonconcurrent varchar(1) NULL DEFAULT NULL, IS_NONCONCURRENT BOOL NULL,
requests_recovery varchar(1) NULL DEFAULT NULL, REQUESTS_RECOVERY BOOL NULL,
PRIMARY KEY (sched_name, entry_id) PRIMARY KEY (SCHED_NAME, ENTRY_ID)
); );
CREATE INDEX idx_qrtz_ft_trig_inst_name ON qrtz_fired_triggers (sched_name, instance_name); CREATE TABLE QRTZ_SCHEDULER_STATE
CREATE INDEX idx_qrtz_ft_inst_job_req_rcvry ON qrtz_fired_triggers (sched_name, instance_name, requests_recovery);
CREATE INDEX idx_qrtz_ft_j_g ON qrtz_fired_triggers (sched_name, job_name, job_group);
CREATE INDEX idx_qrtz_ft_jg ON qrtz_fired_triggers (sched_name, job_group);
CREATE INDEX idx_qrtz_ft_t_g ON qrtz_fired_triggers (sched_name, trigger_name, trigger_group);
CREATE INDEX idx_qrtz_ft_tg ON qrtz_fired_triggers (sched_name, trigger_group);
-- ----------------------------
-- qrtz_job_details
-- ----------------------------
CREATE TABLE qrtz_job_details
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
job_name varchar(190) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL,
job_group varchar(190) NOT NULL, LAST_CHECKIN_TIME BIGINT NOT NULL,
description varchar(250) NULL DEFAULT NULL, CHECKIN_INTERVAL BIGINT NOT NULL,
job_class_name varchar(250) NOT NULL, PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
is_durable varchar(1) NOT NULL,
is_nonconcurrent varchar(1) NOT NULL,
is_update_data varchar(1) NOT NULL,
requests_recovery varchar(1) NOT NULL,
job_data bytea NULL,
PRIMARY KEY (sched_name, job_name, job_group)
); );
CREATE INDEX idx_qrtz_j_req_recovery ON qrtz_job_details (sched_name, requests_recovery); CREATE TABLE QRTZ_LOCKS
CREATE INDEX idx_qrtz_j_grp ON qrtz_job_details (sched_name, job_group);
-- @formatter:off
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- qrtz_locks
-- ----------------------------
CREATE TABLE qrtz_locks
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
lock_name varchar(40) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (sched_name, lock_name) PRIMARY KEY (SCHED_NAME, LOCK_NAME)
); );
-- @formatter:off CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY
BEGIN; ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY);
COMMIT; CREATE INDEX IDX_QRTZ_J_GRP
-- @formatter:on ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP);
-- ---------------------------- CREATE INDEX IDX_QRTZ_T_J
-- qrtz_paused_trigger_grps ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
-- ---------------------------- CREATE INDEX IDX_QRTZ_T_JG
CREATE TABLE qrtz_paused_trigger_grps ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP);
( CREATE INDEX IDX_QRTZ_T_C
sched_name varchar(120) NOT NULL, ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME);
trigger_group varchar(190) NOT NULL, CREATE INDEX IDX_QRTZ_T_G
PRIMARY KEY (sched_name, trigger_group) ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
); CREATE INDEX IDX_QRTZ_T_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME
ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE);
-- ---------------------------- CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME
-- qrtz_scheduler_state ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME);
-- ---------------------------- CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY
CREATE TABLE qrtz_scheduler_state ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY);
( CREATE INDEX IDX_QRTZ_FT_J_G
sched_name varchar(120) NOT NULL, ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
instance_name varchar(190) NOT NULL, CREATE INDEX IDX_QRTZ_FT_JG
last_checkin_time int8 NOT NULL, ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP);
checkin_interval int8 NOT NULL, CREATE INDEX IDX_QRTZ_FT_T_G
PRIMARY KEY (sched_name, instance_name) ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP);
); CREATE INDEX IDX_QRTZ_FT_TG
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
-- @formatter:off
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- qrtz_simple_triggers
-- ----------------------------
CREATE TABLE qrtz_simple_triggers
(
sched_name varchar(120) NOT NULL,
trigger_name varchar(190) NOT NULL,
trigger_group varchar(190) NOT NULL,
repeat_count int8 NOT NULL,
repeat_interval int8 NOT NULL,
times_triggered int8 NOT NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
);
-- ----------------------------
-- qrtz_simprop_triggers
-- ----------------------------
CREATE TABLE qrtz_simprop_triggers
(
sched_name varchar(120) NOT NULL,
trigger_name varchar(190) NOT NULL,
trigger_group varchar(190) NOT NULL,
str_prop_1 varchar(512) NULL DEFAULT NULL,
str_prop_2 varchar(512) NULL DEFAULT NULL,
str_prop_3 varchar(512) NULL DEFAULT NULL,
int_prop_1 int4 NULL DEFAULT NULL,
int_prop_2 int4 NULL DEFAULT NULL,
long_prop_1 int8 NULL DEFAULT NULL,
long_prop_2 int8 NULL DEFAULT NULL,
dec_prop_1 numeric(13, 4) NULL DEFAULT NULL,
dec_prop_2 numeric(13, 4) NULL DEFAULT NULL,
bool_prop_1 varchar(1) NULL DEFAULT NULL,
bool_prop_2 varchar(1) NULL DEFAULT NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
);
-- ----------------------------
-- qrtz_triggers
-- ----------------------------
CREATE TABLE qrtz_triggers
(
sched_name varchar(120) NOT NULL,
trigger_name varchar(190) NOT NULL,
trigger_group varchar(190) NOT NULL,
job_name varchar(190) NOT NULL,
job_group varchar(190) NOT NULL,
description varchar(250) NULL DEFAULT NULL,
next_fire_time int8 NULL DEFAULT NULL,
prev_fire_time int8 NULL DEFAULT NULL,
priority int4 NULL DEFAULT NULL,
trigger_state varchar(16) NOT NULL,
trigger_type varchar(8) NOT NULL,
start_time int8 NOT NULL,
end_time int8 NULL DEFAULT NULL,
calendar_name varchar(190) NULL DEFAULT NULL,
misfire_instr int2 NULL DEFAULT NULL,
job_data bytea NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
);
CREATE INDEX idx_qrtz_t_j ON qrtz_triggers (sched_name, job_name, job_group);
CREATE INDEX idx_qrtz_t_jg ON qrtz_triggers (sched_name, job_group);
CREATE INDEX idx_qrtz_t_c ON qrtz_triggers (sched_name, calendar_name);
CREATE INDEX idx_qrtz_t_g ON qrtz_triggers (sched_name, trigger_group);
CREATE INDEX idx_qrtz_t_state ON qrtz_triggers (sched_name, trigger_state);
CREATE INDEX idx_qrtz_t_n_state ON qrtz_triggers (sched_name, trigger_name, trigger_group, trigger_state);
CREATE INDEX idx_qrtz_t_n_g_state ON qrtz_triggers (sched_name, trigger_group, trigger_state);
CREATE INDEX idx_qrtz_t_next_fire_time ON qrtz_triggers (sched_name, next_fire_time);
CREATE INDEX idx_qrtz_t_nft_st ON qrtz_triggers (sched_name, trigger_state, next_fire_time);
CREATE INDEX idx_qrtz_t_nft_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time);
CREATE INDEX idx_qrtz_t_nft_st_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_state);
CREATE INDEX idx_qrtz_t_nft_st_misfire_grp ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_group,
trigger_state);
-- @formatter:off
BEGIN;
COMMIT;
-- @formatter:on
-- ---------------------------- COMMIT;
-- FK: qrtz_blob_triggers
-- ----------------------------
ALTER TABLE qrtz_blob_triggers
ADD CONSTRAINT qrtz_blob_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name,
trigger_name,
trigger_group);
-- ----------------------------
-- FK: qrtz_cron_triggers
-- ----------------------------
ALTER TABLE qrtz_cron_triggers
ADD CONSTRAINT qrtz_cron_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group);
-- ----------------------------
-- FK: qrtz_simple_triggers
-- ----------------------------
ALTER TABLE qrtz_simple_triggers
ADD CONSTRAINT qrtz_simple_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name,
trigger_name,
trigger_group);
-- ----------------------------
-- FK: qrtz_simprop_triggers
-- ----------------------------
ALTER TABLE qrtz_simprop_triggers
ADD CONSTRAINT qrtz_simprop_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group);
-- ----------------------------
-- FK: qrtz_triggers
-- ----------------------------
ALTER TABLE qrtz_triggers
ADD CONSTRAINT qrtz_triggers_ibfk_1 FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details (sched_name, job_name, job_group);

View File

@@ -18,7 +18,7 @@
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 --> <!-- 统一依赖管理 -->
<spring.framework.version>5.3.39</spring.framework.version> <spring.framework.version>5.3.39</spring.framework.version>
<spring.security.version>5.8.14</spring.security.version> <spring.security.version>5.8.16</spring.security.version>
<spring.boot.version>2.7.18</spring.boot.version> <spring.boot.version>2.7.18</spring.boot.version>
<!-- Web 相关 --> <!-- Web 相关 -->
<springdoc.version>1.8.0</springdoc.version> <springdoc.version>1.8.0</springdoc.version>
@@ -76,7 +76,8 @@
<awssdk.version>2.30.14</awssdk.version> <awssdk.version>2.30.14</awssdk.version>
<justauth.version>1.16.7</justauth.version> <justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version> <justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>1.9.4</jimureport.version> <jimureport.version>2.1.0</jimureport.version>
<jimubi.version>1.9.5</jimubi.version>
<weixin-java.version>4.7.5.B</weixin-java.version> <weixin-java.version>4.7.5.B</weixin-java.version>
<!-- 专属于 JDK8 安全漏洞升级 --> <!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 --> <logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
@@ -626,7 +627,7 @@
<dependency> <dependency>
<groupId>org.jeecgframework.jimureport</groupId> <groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot-starter</artifactId> <artifactId>jimubi-spring-boot-starter</artifactId>
<version>${jimureport.version}</version> <version>${jimubi.version}</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>com.github.jsqlparser</groupId> <groupId>com.github.jsqlparser</groupId>

View File

@@ -7,6 +7,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
/** /**
* 异步任务 Configuration * 异步任务 Configuration
@@ -21,13 +22,20 @@ public class YudaoAsyncAutoConfiguration {
@Override @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof ThreadPoolTaskExecutor)) { // 处理 ThreadPoolTaskExecutor
return bean; if (bean instanceof ThreadPoolTaskExecutor) {
ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean;
executor.setTaskDecorator(TtlRunnable::get);
return executor;
} }
// 修改提交的任务,接入 TransmittableThreadLocal // 处理 SimpleAsyncTaskExecutor
ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; // 参考 https://t.zsxq.com/CBoks 增加
executor.setTaskDecorator(TtlRunnable::get); if (bean instanceof SimpleAsyncTaskExecutor) {
return executor; SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) bean;
executor.setTaskDecorator(TtlRunnable::get);
return executor;
}
return bean;
} }
}; };

View File

@@ -24,8 +24,8 @@
<!-- Web 相关 --> <!-- Web 相关 -->
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId> <artifactId>yudao-spring-boot-starter-security</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 OncePerRequestFilter 使用到 --> <scope>provided</scope> <!-- 设置为 provided只有 DefaultDBFieldHandler 使用到 -->
</dependency> </dependency>
<!-- DB 相关 --> <!-- DB 相关 -->

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.mybatis.core.handler; package cn.iocoder.yudao.framework.mybatis.core.handler;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.MetaObject;
@@ -32,7 +32,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
baseDO.setUpdateTime(current); baseDO.setUpdateTime(current);
} }
Long userId = WebFrameworkUtils.getLoginUserId(); Long userId = SecurityFrameworkUtils.getLoginUserId();
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人 // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
baseDO.setCreator(userId.toString()); baseDO.setCreator(userId.toString());
@@ -54,7 +54,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人 // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
Object modifier = getFieldValByName("updater", metaObject); Object modifier = getFieldValByName("updater", metaObject);
Long userId = WebFrameworkUtils.getLoginUserId(); Long userId = SecurityFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) { if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updater", userId.toString(), metaObject); setFieldValByName("updater", userId.toString(), metaObject);
} }

View File

@@ -42,6 +42,7 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) { default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) {
// 特殊:不分页,直接查询全部 // 特殊:不分页,直接查询全部
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
MyBatisUtils.addOrder(queryWrapper, sortingFields);
List<T> list = selectList(queryWrapper); List<T> list = selectList(queryWrapper);
return new PageResult<>(list, (long) list.size()); return new PageResult<>(list, (long) list.size());
} }

View File

@@ -1,6 +1,6 @@
package cn.iocoder.yudao.framework.mybatis.core.util; package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.func.Func1; import cn.hutool.core.lang.func.Func1;
import cn.hutool.core.lang.func.LambdaUtil; import cn.hutool.core.lang.func.LambdaUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@@ -8,6 +8,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField; import cn.iocoder.yudao.framework.common.pojo.SortingField;
import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
@@ -20,7 +22,6 @@ import net.sf.jsqlparser.schema.Table;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* MyBatis 工具类 * MyBatis 工具类
@@ -37,15 +38,27 @@ public class MyBatisUtils {
// 页码 + 数量 // 页码 + 数量
Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
// 排序字段 // 排序字段
if (!CollectionUtil.isEmpty(sortingFields)) { if (CollUtil.isNotEmpty(sortingFields)) {
page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) for (SortingField sortingField : sortingFields) {
? OrderItem.asc(StrUtil.toUnderlineCase(sortingField.getField())) page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder()))
: OrderItem.desc(StrUtil.toUnderlineCase(sortingField.getField()))) .setColumn(StrUtil.toUnderlineCase(sortingField.getField())));
.collect(Collectors.toList())); }
} }
return page; return page;
} }
public static <T> void addOrder(Wrapper<T> wrapper, Collection<SortingField> sortingFields) {
if (CollUtil.isEmpty(sortingFields)) {
return;
}
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
for (SortingField sortingField : sortingFields) {
query.orderBy(true,
SortingField.ORDER_ASC.equals(sortingField.getOrder()),
StrUtil.toUnderlineCase(sortingField.getField()));
}
}
/** /**
* 将拦截器添加到链中 * 将拦截器添加到链中
* 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置

View File

@@ -126,8 +126,10 @@ public class SecurityFrameworkUtils {
// 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
// 原因是Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 // 原因是Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); if (request != null) {
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
}
} }
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {

View File

@@ -19,7 +19,8 @@
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno 国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description> </description>
<properties> <properties>
<spring-ai.version>1.0.0-M6</spring-ai.version> <spring-ai.version>1.0.0</spring-ai.version>
<alibaba-ai.version>1.0.0.2</alibaba-ai.version>
<tinyflow.version>1.0.2</tinyflow.version> <tinyflow.version>1.0.2</tinyflow.version>
</properties> </properties>
@@ -75,65 +76,73 @@
<!-- Spring AI Model 模型接入 --> <!-- Spring AI Model 模型接入 -->
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId> <artifactId>spring-ai-starter-model-openai</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId> <artifactId>spring-ai-starter-model-azure-openai</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId> <artifactId>spring-ai-starter-model-deepseek</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId> <artifactId>spring-ai-starter-model-ollama</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<!-- 通义千问 -->
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-ai.version}.1</version>
</dependency>
<dependency>
<!-- 文心一言 -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qianfan-spring-boot-starter</artifactId> <artifactId>spring-ai-starter-model-stability-ai</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<!-- 智谱 GLM --> <!-- 智谱 GLM -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId> <artifactId>spring-ai-starter-model-zhipuai</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-minimax-spring-boot-starter</artifactId> <artifactId>spring-ai-starter-model-minimax</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <!-- 通义千问 -->
<artifactId>spring-ai-moonshot-spring-boot-starter</artifactId> <groupId>com.alibaba.cloud.ai</groupId>
<version>${spring-ai.version}</version> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>${alibaba-ai.version}</version>
</dependency>
<dependency>
<!-- 文心一言 -->
<groupId>org.springaicommunity</groupId>
<artifactId>qianfan-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<!-- 月之暗灭 -->
<groupId>org.springaicommunity</groupId>
<artifactId>moonshot-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency> </dependency>
<!-- 向量存储https://db-engines.com/en/ranking/vector+dbms --> <!-- 向量存储https://db-engines.com/en/ranking/vector+dbms -->
<dependency> <dependency>
<!-- Qdranthttps://qdrant.tech/ --> <!-- Qdranthttps://qdrant.tech/ -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qdrant-store</artifactId> <artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<!-- Redishttps://redis.io/docs/latest/develop/get-started/vector-database/ --> <!-- Redishttps://redis.io/docs/latest/develop/get-started/vector-database/ -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-redis-store</artifactId> <artifactId>spring-ai-starter-vector-store-redis</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -144,7 +153,7 @@
<dependency> <dependency>
<!-- Milvushttps://milvus.io/ --> <!-- Milvushttps://milvus.io/ -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store</artifactId> <artifactId>spring-ai-starter-vector-store-milvus</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
<exclusions> <exclusions>
<!-- 解决和 logback 的日志冲突 --> <!-- 解决和 logback 的日志冲突 -->

View File

@@ -5,7 +5,6 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory; import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory;
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl; import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
@@ -14,10 +13,6 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.model.tool.ToolCallingManager;
@@ -26,6 +21,10 @@ import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -52,33 +51,6 @@ public class AiAutoConfiguration {
// ========== 各种 AI Client 创建 ========== // ========== 各种 AI Client 创建 ==========
@Bean
@ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true")
public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepseek();
return buildDeepSeekChatModel(properties);
}
public DeepSeekChatModel buildDeepSeekChatModel(YudaoAiProperties.DeepSeekProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(DeepSeekChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(DeepSeekChatModel.BASE_URL)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new DeepSeekChatModel(openAiChatModel);
}
@Bean @Bean
@ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {

View File

@@ -13,12 +13,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@Data @Data
public class YudaoAiProperties { public class YudaoAiProperties {
/**
* DeepSeek
*/
@SuppressWarnings("SpellCheckingInspection")
private DeepSeekProperties deepseek;
/** /**
* 字节豆包 * 字节豆包
*/ */
@@ -60,19 +54,6 @@ public class YudaoAiProperties {
@SuppressWarnings("SpellCheckingInspection") @SuppressWarnings("SpellCheckingInspection")
private SunoProperties suno; private SunoProperties suno;
@Data
public static class DeepSeekProperties {
private String enable;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data @Data
public static class DouBaoProperties { public static class DouBaoProperties {

View File

@@ -8,11 +8,11 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.RuntimeUtil; import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration; import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration;
import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
@@ -22,8 +22,9 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration; import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
@@ -32,47 +33,55 @@ import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.KeyCredential;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import io.milvus.client.MilvusServiceClient; import io.milvus.client.MilvusServiceClient;
import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient; import io.qdrant.client.QdrantGrpcClient;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration; import org.springaicommunity.moonshot.MoonshotChatModel;
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties; import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties; import org.springaicommunity.moonshot.api.MoonshotApi;
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiEmbeddingProperties; import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
import org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration; import org.springaicommunity.qianfan.QianFanChatModel;
import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration; import org.springaicommunity.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; import org.springaicommunity.qianfan.QianFanEmbeddingOptions;
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; import org.springaicommunity.qianfan.QianFanImageModel;
import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; import org.springaicommunity.qianfan.api.QianFanApi;
import org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration; import org.springaicommunity.qianfan.api.QianFanImageApi;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails; import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties; import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import org.springframework.ai.document.MetadataMode; import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageModel;
import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
import org.springframework.ai.minimax.api.MiniMaxApi; import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.function.FunctionCallbackResolver; import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration;
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration;
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration;
import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration;
import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration;
import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.moonshot.MoonshotChatModel; import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration;
import org.springframework.ai.moonshot.MoonshotChatOptions; import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration;
import org.springframework.ai.moonshot.api.MoonshotApi; import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration;
import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi; import org.springframework.ai.ollama.api.OllamaApi;
@@ -84,21 +93,23 @@ import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiImageApi; import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.api.common.OpenAiApiConstants; import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.qianfan.QianFanEmbeddingOptions;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.ai.stabilityai.StabilityAiImageModel; import org.springframework.ai.stabilityai.StabilityAiImageModel;
import org.springframework.ai.stabilityai.api.StabilityAiApi; import org.springframework.ai.stabilityai.api.StabilityAiApi;
import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore; import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientConnectionDetails;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention; import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
import org.springframework.ai.vectorstore.redis.RedisVectorStore; import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
import org.springframework.ai.zhipuai.*; import org.springframework.ai.zhipuai.*;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
@@ -190,7 +201,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
case XING_HUO: case XING_HUO:
return SpringUtil.getBean(XingHuoChatModel.class); return SpringUtil.getBean(XingHuoChatModel.class);
case BAI_CHUAN: case BAI_CHUAN:
return SpringUtil.getBean(AzureOpenAiChatModel.class); return SpringUtil.getBean(BaiChuanChatModel.class);
case OPENAI: case OPENAI:
return SpringUtil.getBean(OpenAiChatModel.class); return SpringUtil.getBean(OpenAiChatModel.class);
case AZURE_OPENAI: case AZURE_OPENAI:
@@ -319,27 +330,34 @@ public class AiModelFactoryImpl implements AiModelFactory {
// ========== 各种创建 spring-ai 客户端的方法 ========== // ========== 各种创建 spring-ai 客户端的方法 ==========
/** /**
* 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法 * 可参考 {@link DashScopeChatAutoConfiguration} 的 dashscopeChatModel 方法
*/ */
private static DashScopeChatModel buildTongYiChatModel(String key) { private static DashScopeChatModel buildTongYiChatModel(String key) {
DashScopeApi dashScopeApi = new DashScopeApi(key); DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(key).build();
DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL) DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL)
.withTemperature(0.7).build(); .withTemperature(0.7).build();
return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); return DashScopeChatModel.builder()
.dashScopeApi(dashScopeApi)
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.build();
} }
/** /**
* 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法 * 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法
*/ */
private static DashScopeImageModel buildTongYiImagesModel(String key) { private static DashScopeImageModel buildTongYiImagesModel(String key) {
DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key);
return new DashScopeImageModel(dashScopeImageApi); return DashScopeImageModel.builder()
.dashScopeApi(dashScopeImageApi)
.build();
} }
/** /**
* 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法 * 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法
*/ */
private static QianFanChatModel buildYiYanChatModel(String key) { private static QianFanChatModel buildYiYanChatModel(String key) {
// TODO spring ai qianfan 有 bug无法使用 https://github.com/spring-ai-community/qianfan/issues/6
List<String> keys = StrUtil.split(key, '|'); List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0); String appKey = keys.get(0);
@@ -349,9 +367,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法 * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法
*/ */
private QianFanImageModel buildQianFanImageModel(String key) { private QianFanImageModel buildQianFanImageModel(String key) {
// TODO spring ai qianfan 有 bug无法使用 https://github.com/spring-ai-community/qianfan/issues/6
List<String> keys = StrUtil.split(key, '|'); List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0); String appKey = keys.get(0);
@@ -361,12 +380,17 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link AiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)} * 可参考 {@link DeepSeekChatAutoConfiguration} 的 deepSeekChatModel 方法
*/ */
private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) {
YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties() DeepSeekApi deepSeekApi = DeepSeekApi.builder().apiKey(apiKey).build();
.setApiKey(apiKey); DeepSeekChatOptions options = DeepSeekChatOptions.builder().model(DeepSeekApi.DEFAULT_CHAT_MODEL)
return new AiAutoConfiguration().buildDeepSeekChatModel(properties); .temperature(0.7).build();
return DeepSeekChatModel.builder()
.deepSeekApi(deepSeekApi)
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.build();
} }
/** /**
@@ -397,17 +421,18 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 * 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法
*/ */
private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
: new ZhiPuAiApi(url, apiKey); : new ZhiPuAiApi(url, apiKey);
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); return new ZhiPuAiChatModel(zhiPuAiApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE,
getObservationRegistry().getIfAvailable());
} }
/** /**
* 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法 * 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法
*/ */
private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {
ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey) ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey)
@@ -416,23 +441,30 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxChatModel 方法 * 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法
*/ */
private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey)
: new MiniMaxApi(url, apiKey); : new MiniMaxApi(url, apiKey);
MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE);
} }
/** /**
* 可参考 {@link MoonshotAutoConfiguration} 的 moonshotChatModel 方法 * 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法
*/ */
private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {
MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey) MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder()
: new MoonshotApi(url, apiKey); .apiKey(apiKey);
if (StrUtil.isNotEmpty(url)) {
moonshotApiBuilder.baseUrl(url);
}
MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build();
return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); return MoonshotChatModel.builder()
.moonshotApi(moonshotApiBuilder.build())
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.build();
} }
/** /**
@@ -456,33 +488,32 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法 * 可参考 {@link OpenAiChatAutoConfiguration} 的 openAiChatModel 方法
*/ */
private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build(); return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.toolCallingManager(getToolCallingManager())
.build();
} }
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
/** /**
* 可参考 {@link AzureOpenAiAutoConfiguration} * 可参考 {@link AzureOpenAiChatAutoConfiguration}
*/ */
private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) { private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration(); // TODO @芋艿:使用前,请测试,暂时没密钥!!!
// 创建 OpenAIClient 对象 OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); .endpoint(url).credential(new KeyCredential(apiKey));
connectionProperties.setApiKey(apiKey); return AzureOpenAiChatModel.builder()
connectionProperties.setEndpoint(url); .openAIClientBuilder(openAIClientBuilder)
OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); .toolCallingManager(getToolCallingManager())
// 获取 AzureOpenAiChatProperties 对象 .build();
AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties,
getToolCallingManager(), null, null);
} }
/** /**
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法 * 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法
*/ */
private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
@@ -500,11 +531,14 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法 * 可参考 {@link OllamaChatAutoConfiguration} 的 ollamaChatModel 方法
*/ */
private static OllamaChatModel buildOllamaChatModel(String url) { private static OllamaChatModel buildOllamaChatModel(String url) {
OllamaApi ollamaApi = new OllamaApi(url); OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build();
return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); return OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.toolCallingManager(getToolCallingManager())
.build();
} }
/** /**
@@ -519,16 +553,16 @@ public class AiModelFactoryImpl implements AiModelFactory {
// ========== 各种创建 EmbeddingModel 的方法 ========== // ========== 各种创建 EmbeddingModel 的方法 ==========
/** /**
* 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 * 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 dashscopeEmbeddingModel 方法
*/ */
private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) {
DashScopeApi dashScopeApi = new DashScopeApi(apiKey); DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build();
DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build(); DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build();
return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions);
} }
/** /**
* 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 * 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法
*/ */
private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) {
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
@@ -538,7 +572,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxEmbeddingModel 方法 * 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法
*/ */
private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) { private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey) MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey)
@@ -548,7 +582,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link QianFanAutoConfiguration} 的 qianFanEmbeddingModel 方法 * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法
*/ */
private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) { private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) {
List<String> keys = StrUtil.split(key, '|'); List<String> keys = StrUtil.split(key, '|');
@@ -561,13 +595,16 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) {
OllamaApi ollamaApi = new OllamaApi(url); OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build();
OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build(); OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build();
return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build(); return OllamaEmbeddingModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(ollamaOptions)
.build();
} }
/** /**
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiEmbeddingModel 方法 * 可参考 {@link OpenAiEmbeddingAutoConfiguration} 的 openAiEmbeddingModel 方法
*/ */
private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) { private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
@@ -576,21 +613,19 @@ public class AiModelFactoryImpl implements AiModelFactory {
return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties); return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties);
} }
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
/** /**
* 可参考 {@link AzureOpenAiAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法 * 可参考 {@link AzureOpenAiEmbeddingAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法
*/ */
private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) { private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) {
AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration(); // TODO @芋艿:手头暂时没密钥,使用建议再测试下
// 创建 OpenAIClient 对象 AzureOpenAiEmbeddingAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiEmbeddingAutoConfiguration();
AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); // 创建 OpenAIClientBuilder 对象
connectionProperties.setApiKey(apiKey); OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
connectionProperties.setEndpoint(url); .endpoint(url).credential(new KeyCredential(apiKey));
OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null);
// 获取 AzureOpenAiChatProperties 对象 // 获取 AzureOpenAiChatProperties 对象
AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class); AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class);
return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClient, embeddingProperties, return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClientBuilder, embeddingProperties,
null, null); getObservationRegistry(), getEmbeddingModelObservationConvention());
} }
// ========== 各种创建 VectorStore 的方法 ========== // ========== 各种创建 VectorStore 的方法 ==========
@@ -655,12 +690,12 @@ public class AiModelFactoryImpl implements AiModelFactory {
Map<String, Class<?>> metadataFields) { Map<String, Class<?>> metadataFields) {
// 创建 JedisPooled 对象 // 创建 JedisPooled 对象
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort()); JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort(),
redisProperties.getUsername(), redisProperties.getPassword());
// 创建 RedisVectorStoreProperties 对象 // 创建 RedisVectorStoreProperties 对象
RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration();
RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class); RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class);
RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel) RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel)
.indexName(properties.getIndex()).prefix(properties.getPrefix()) .indexName(properties.getIndexName()).prefix(properties.getPrefix())
.initializeSchema(properties.isInitializeSchema()) .initializeSchema(properties.isInitializeSchema())
.metadataFields(convertList(metadataFields.entrySet(), entry -> { .metadataFields(convertList(metadataFields.entrySet(), entry -> {
String fieldName = entry.getKey(); String fieldName = entry.getKey();
@@ -730,10 +765,12 @@ public class AiModelFactoryImpl implements AiModelFactory {
private static ObjectProvider<VectorStoreObservationConvention> getCustomObservationConvention() { private static ObjectProvider<VectorStoreObservationConvention> getCustomObservationConvention() {
return new ObjectProvider<>() { return new ObjectProvider<>() {
@Override @Override
public VectorStoreObservationConvention getObject() throws BeansException { public VectorStoreObservationConvention getObject() throws BeansException {
return new DefaultVectorStoreObservationConvention(); return new DefaultVectorStoreObservationConvention();
} }
}; };
} }
@@ -745,8 +782,15 @@ public class AiModelFactoryImpl implements AiModelFactory {
return SpringUtil.getBean(ToolCallingManager.class); return SpringUtil.getBean(ToolCallingManager.class);
} }
private static FunctionCallbackResolver getFunctionCallbackResolver() { private static ObjectProvider<EmbeddingModelObservationConvention> getEmbeddingModelObservationConvention() {
return SpringUtil.getBean(FunctionCallbackResolver.class); return new ObjectProvider<>() {
@Override
public EmbeddingModelObservationConvention getObject() throws BeansException {
return SpringUtil.getBean(EmbeddingModelObservationConvention.class);
}
};
} }
} }

View File

@@ -1,45 +0,0 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* DeepSeek {@link ChatModel} 实现类
*
* @author fansili
*/
@Slf4j
@RequiredArgsConstructor
public class DeepSeekChatModel implements ChatModel {
public static final String BASE_URL = "https://api.deepseek.com";
public static final String MODEL_DEFAULT = "deepseek-chat";
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}

View File

@@ -89,7 +89,7 @@ public class SiliconFlowImageModel implements ImageModel {
var observationContext = ImageModelObservationContext.builder() var observationContext = ImageModelObservationContext.builder()
.imagePrompt(imagePrompt) .imagePrompt(imagePrompt)
.provider(SiliconFlowApiConstants.PROVIDER_NAME) .provider(SiliconFlowApiConstants.PROVIDER_NAME)
.requestOptions(imagePrompt.getOptions()) .imagePrompt(imagePrompt)
.build(); .build();
return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION

View File

@@ -9,9 +9,6 @@ import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
@@ -24,17 +21,20 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper;
import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import cn.iocoder.yudao.module.infra.api.file.FileApi; import cn.iocoder.yudao.module.infra.api.file.FileApi;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springaicommunity.qianfan.QianFanImageOptions;
import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImageOptions;
import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse; import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.openai.OpenAiImageOptions; import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.ai.qianfan.QianFanImageOptions;
import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
@@ -140,10 +140,10 @@ public class AiImageServiceImpl implements AiImageService {
private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) { private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) {
if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) {
// https://platform.openai.com/docs/api-reference/images/create // https://platform.openai.com/docs/api-reference/images/create
return OpenAiImageOptions.builder().withModel(model.getModel()) return OpenAiImageOptions.builder().model(model.getModel())
.withHeight(draw.getHeight()).withWidth(draw.getWidth()) .height(draw.getHeight()).width(draw.getWidth())
.withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格 .style(MapUtil.getStr(draw.getOptions(), "style")) // 风格
.withResponseFormat("b64_json") .responseFormat("b64_json")
.build(); .build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) { } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) {
// https://docs.siliconflow.cn/cn/api-reference/images/images-generations // https://docs.siliconflow.cn/cn/api-reference/images/images-generations

View File

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -17,7 +19,7 @@ import org.springframework.stereotype.Component;
import java.util.function.BiFunction; import java.util.function.BiFunction;
/** /**
* 工具:当前用户信息查询 * 工具:用户信息查询
* *
* 同时,也是展示 ToolContext 上下文的使用 * 同时,也是展示 ToolContext 上下文的使用
* *
@@ -31,8 +33,17 @@ public class UserProfileQueryToolFunction
private AdminUserApi adminUserApi; private AdminUserApi adminUserApi;
@Data @Data
@JsonClassDescription("当前用户信息查询") @JsonClassDescription("用户信息查询")
public static class Request { } public static class Request {
/**
* 用户编号
*/
@JsonProperty(value = "id")
@JsonPropertyDescription("用户编号例如说1。如果查询自己则 id 为空")
private Long id;
}
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@@ -61,13 +72,19 @@ public class UserProfileQueryToolFunction
@Override @Override
public Response apply(Request request, ToolContext toolContext) { public Response apply(Request request, ToolContext toolContext) {
LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER);
Long tenantId = (Long) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_TENANT_ID); Long tenantId = (Long) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_TENANT_ID);
if (loginUser == null | tenantId == null) { if (tenantId == null) {
return null; return new Response();
}
if (request.getId() == null) {
LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER);
if (loginUser == null) {
return new Response();
}
request.setId(loginUser.getId());
} }
return TenantUtils.execute(tenantId, () -> { return TenantUtils.execute(tenantId, () -> {
AdminUserRespDTO user = adminUserApi.getUser(loginUser.getId()); AdminUserRespDTO user = adminUserApi.getUser(request.getId());
return BeanUtils.toBean(user, Response.class); return BeanUtils.toBean(user, Response.class);
}); });
} }

View File

@@ -2,18 +2,18 @@ package cn.iocoder.yudao.module.ai.util;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.qianfan.QianFanChatOptions;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.qianfan.QianFanChatOptions;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import java.util.Collections; import java.util.Collections;
@@ -43,18 +43,18 @@ public class AiUtils {
switch (platform) { switch (platform) {
case TONG_YI: case TONG_YI:
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
.withFunctions(toolNames).withToolContext(toolContext).build(); .withToolNames(toolNames).withToolContext(toolContext).build();
case YI_YAN: case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case ZHI_PU: case ZHI_PU:
return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.functions(toolNames).toolContext(toolContext).build(); .toolNames(toolNames).toolContext(toolContext).build();
case MINI_MAX: case MINI_MAX:
return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.functions(toolNames).toolContext(toolContext).build(); .toolNames(toolNames).toolContext(toolContext).build();
case MOONSHOT: case MOONSHOT:
return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.functions(toolNames).toolContext(toolContext).build(); .toolNames(toolNames).toolContext(toolContext).build();
case OPENAI: case OPENAI:
case DEEP_SEEK: // 复用 OpenAI 客户端 case DEEP_SEEK: // 复用 OpenAI 客户端
case DOU_BAO: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端

View File

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential; import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.util.ClientOptions;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
@@ -17,7 +16,7 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME; import static org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
/** /**
* {@link AzureOpenAiChatModel} 集成测试 * {@link AzureOpenAiChatModel} 集成测试
@@ -29,10 +28,13 @@ public class AzureOpenAIChatModelTests {
// TODO @芋艿:晚点在调整 // TODO @芋艿:晚点在调整
private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder() private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder()
.endpoint("https://eastusprejade.openai.azure.com") .endpoint("https://eastusprejade.openai.azure.com")
.credential(new AzureKeyCredential("xxx")) .credential(new AzureKeyCredential("xxx"));
.clientOptions((new ClientOptions()).setApplicationId("spring-ai")); private final AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder()
private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi, .openAIClientBuilder(openAiApi)
AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build()); .defaultOptions(AzureOpenAiChatOptions.builder()
.deploymentName(DEFAULT_DEPLOYMENT_NAME)
.build())
.build();
@Test @Test
@Disabled @Disabled

View File

@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
@@ -35,7 +34,7 @@ public class BaiChuanChatModelTests {
.build()) .build())
.build(); .build();
private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel);
@Test @Test
@Disabled @Disabled

View File

@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
@@ -8,9 +7,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@@ -23,19 +22,16 @@ import java.util.List;
*/ */
public class DeepSeekChatModelTests { public class DeepSeekChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder()
.openAiApi(OpenAiApi.builder() .deepSeekApi(DeepSeekApi.builder()
.baseUrl(DeepSeekChatModel.BASE_URL) .apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey
.apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(DeepSeekChatOptions.builder()
.model("deepseek-chat") // 模型 .model("deepseek-chat") // 模型
.temperature(0.7) .temperature(0.7)
.build()) .build())
.build(); .build();
private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel);
@Test @Test
@Disabled @Disabled
public void testCall() { public void testCall() {

View File

@@ -1,20 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaModel;
import org.springframework.ai.ollama.api.OllamaOptions;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/** /**
* {@link OllamaChatModel} 集成测试 * {@link OllamaChatModel} 集成测试
@@ -23,43 +9,43 @@ import java.util.List;
*/ */
public class LlamaChatModelTests { public class LlamaChatModelTests {
private final OllamaChatModel chatModel = OllamaChatModel.builder() // private final OllamaChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 // .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址
.defaultOptions(OllamaOptions.builder() // .defaultOptions(OllamaOptions.builder()
.model(OllamaModel.LLAMA3.getName()) // 模型 // .model(OllamaModel.LLAMA3.getName()) // 模型
.build()) // .build())
.build(); // .build();
//
@Test // @Test
@Disabled // @Disabled
public void testCall() { // public void testCall() {
// 准备参数 // // 准备参数
List<Message> messages = new ArrayList<>(); // List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); // messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ")); // messages.add(new UserMessage("1 + 1 = "));
//
// 调用 // // 调用
ChatResponse response = chatModel.call(new Prompt(messages)); // ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果 // // 打印结果
System.out.println(response); // System.out.println(response);
System.out.println(response.getResult().getOutput()); // System.out.println(response.getResult().getOutput());
} // }
//
@Test // @Test
@Disabled // @Disabled
public void testStream() { // public void testStream() {
// 准备参数 // // 准备参数
List<Message> messages = new ArrayList<>(); // List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); // messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = ")); // messages.add(new UserMessage("1 + 1 = "));
//
// 调用 // // 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); // Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果 // // 打印结果
flux.doOnNext(response -> { // flux.doOnNext(response -> {
// System.out.println(response); //// System.out.println(response);
System.out.println(response.getResult().getOutput()); // System.out.println(response.getResult().getOutput());
}).then().block(); // }).then().block();
} // }
} }

View File

@@ -2,14 +2,14 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springaicommunity.moonshot.MoonshotChatModel;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.moonshot.api.MoonshotApi;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.moonshot.api.MoonshotApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@@ -22,11 +22,15 @@ import java.util.List;
*/ */
public class MoonshotChatModelTests { public class MoonshotChatModelTests {
private final MoonshotChatModel chatModel = new MoonshotChatModel( private final MoonshotChatModel chatModel = MoonshotChatModel.builder()
new MoonshotApi("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA"), // 密钥 .moonshotApi(MoonshotApi.builder()
MoonshotChatOptions.builder() .apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥
.model("moonshot-v1-8k") // 模型 .build())
.build()); .defaultOptions(MoonshotChatOptions.builder()
.model("kimi-k2-0711-preview") // 模型
.build())
.build();
@Test @Test
@Disabled @Disabled
public void testCall() { public void testCall() {

View File

@@ -23,7 +23,9 @@ import java.util.List;
public class OllamaChatModelTests { public class OllamaChatModelTests {
private final OllamaChatModel chatModel = OllamaChatModel.builder() private final OllamaChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 .ollamaApi(OllamaApi.builder()
.baseUrl("http://127.0.0.1:11434") // Ollama 服务地址
.build())
.defaultOptions(OllamaOptions.builder() .defaultOptions(OllamaOptions.builder()
// .model("qwen") // 模型https://ollama.com/library/qwen // .model("qwen") // 模型https://ollama.com/library/qwen
.model("deepseek-r1") // 模型https://ollama.com/library/deepseek-r1 .model("deepseek-r1") // 模型https://ollama.com/library/deepseek-r1

View File

@@ -25,10 +25,10 @@ public class OpenAIChatModelTests {
private final OpenAiChatModel chatModel = OpenAiChatModel.builder() private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder() .openAiApi(OpenAiApi.builder()
.baseUrl("https://api.holdai.top") .baseUrl("https://api.holdai.top")
.apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") // apiKey .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(OpenAiChatOptions.builder()
.model(OpenAiApi.ChatModel.GPT_4_O) // 模型 .model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型
.temperature(0.7) .temperature(0.7)
.build()) .build())
.build(); .build();

View File

@@ -22,14 +22,17 @@ import java.util.List;
*/ */
public class TongYiChatModelTests { public class TongYiChatModelTests {
private final DashScopeChatModel chatModel = new DashScopeChatModel( private final DashScopeChatModel chatModel = DashScopeChatModel.builder()
new DashScopeApi("sk-7d903764249848cfa912733146da12d1"), .dashScopeApi(DashScopeApi.builder()
DashScopeChatOptions.builder() .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build())
.defaultOptions( DashScopeChatOptions.builder()
.withModel("qwen1.5-72b-chat") // 模型 .withModel("qwen1.5-72b-chat") // 模型
// .withModel("deepseek-r1") // 模型deepseek-r1 // .withModel("deepseek-r1") // 模型deepseek-r1
// .withModel("deepseek-v3") // 模型deepseek-v3 // .withModel("deepseek-v3") // 模型deepseek-v3
// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b
.build()); .build())
.build();
@Test @Test
@Disabled @Disabled

View File

@@ -2,13 +2,13 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springaicommunity.qianfan.QianFanChatModel;
import org.springaicommunity.qianfan.QianFanChatOptions;
import org.springaicommunity.qianfan.api.QianFanApi;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanChatOptions;
import org.springframework.ai.qianfan.api.QianFanApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@@ -23,9 +23,9 @@ import java.util.List;
public class YiYanChatModelTests { public class YiYanChatModelTests {
private final QianFanChatModel chatModel = new QianFanChatModel( private final QianFanChatModel chatModel = new QianFanChatModel(
new QianFanApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"), // 密钥 new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥
QianFanChatOptions.builder() QianFanChatOptions.builder()
.model(QianFanApi.ChatModel.ERNIE_4_0_8K_Preview.getValue()) .model("ERNIE-4.5-8K-Preview")
.build() .build()
); );

View File

@@ -18,7 +18,7 @@ public class OpenAiImageModelTests {
private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder()
.baseUrl("https://api.holdai.top") // apiKey .baseUrl("https://api.holdai.top") // apiKey
.apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD")
.build()); .build());
@Test @Test
@@ -26,8 +26,8 @@ public class OpenAiImageModelTests {
public void testCall() { public void testCall() {
// 准备参数 // 准备参数
ImageOptions options = OpenAiImageOptions.builder() ImageOptions options = OpenAiImageOptions.builder()
.withModel(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 .model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜
.withHeight(256).withWidth(256) .height(256).width(256)
.build(); .build();
ImagePrompt prompt = new ImagePrompt("中国长城!", options); ImagePrompt prompt = new ImagePrompt("中国长城!", options);

View File

@@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springaicommunity.qianfan.QianFanImageModel;
import org.springaicommunity.qianfan.QianFanImageOptions;
import org.springaicommunity.qianfan.api.QianFanImageApi;
import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse; import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.QianFanImageOptions;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage; import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage;

View File

@@ -31,8 +31,8 @@ public class StabilityAiImageModelTests {
public void testCall() { public void testCall() {
// 准备参数 // 准备参数
ImageOptions options = OpenAiImageOptions.builder() ImageOptions options = OpenAiImageOptions.builder()
.withModel("stable-diffusion-v1-6") .model("stable-diffusion-v1-6")
.withHeight(320).withWidth(320) .height(320).width(320)
.build(); .build();
ImagePrompt prompt = new ImagePrompt("great wall", options); ImagePrompt prompt = new ImagePrompt("great wall", options);

View File

@@ -62,6 +62,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -173,7 +174,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
/** /**
* 获得用户指定 taskId 任务编号的待办(未审批、且可审核)的任务 * 获得用户指定 taskId 任务编号的"待办"(未审批、且可审核)的任务
* *
* @param userId 用户编号 * @param userId 用户编号
* @param taskId 任务编号 * @param taskId 任务编号
@@ -194,7 +195,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
/** /**
* 获得用户指定 processInstanceId 流程编号下的首个待办(未审批、且可审核)的任务 * 获得用户指定 processInstanceId 流程编号下的首个"待办"(未审批、且可审核)的任务
* *
* @param userId 用户编号 * @param userId 用户编号
* @param processInstanceId 流程编号 * @param processInstanceId 流程编号
@@ -241,7 +242,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
List<HistoricTaskInstance> tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); List<HistoricTaskInstance> tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
// 特殊:强制移除自动完成的发起人节点 // 特殊:强制移除自动完成的"发起人"节点
// 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤 // 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤
tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID)); tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID));
return new PageResult<>(tasks, count); return new PageResult<>(tasks, count);
@@ -297,7 +298,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
public Task validateTask(Long userId, String taskId) { public Task validateTask(Long userId, String taskId) {
Task task = validateTaskExist(taskId); Task task = validateTaskExist(taskId);
// 为什么判断 assignee 非空的情况下? // 为什么判断 assignee 非空的情况下?
// 例如说:在审批人为空时,我们会有自动审批通过的策略,此时 userId 为 null允许通过 // 例如说:在审批人为空时,我们会有"自动审批通过"的策略,此时 userId 为 null允许通过
if (StrUtil.isNotBlank(task.getAssignee()) if (StrUtil.isNotBlank(task.getAssignee())
&& ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) { && ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) {
throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
@@ -404,19 +405,28 @@ public class BpmTaskServiceImpl implements BpmTaskService {
* @return 所有子任务列表 * @return 所有子任务列表
*/ */
private List<Task> getAllChildTaskList(Task parentTask) { private List<Task> getAllChildTaskList(Task parentTask) {
// 1. 获取流程实例的所有任务
List<Task> allTasks = taskService.createTaskQuery().processInstanceId(parentTask.getProcessInstanceId()).list();
if (CollUtil.isEmpty(allTasks)) {
return Collections.emptyList();
}
// 2. 构建父任务到子任务列表的映射关系,用于内存中快速查找
Map<String, List<Task>> childrenTaskMap = allTasks.stream()
.filter(task -> task.getParentTaskId() != null)
.collect(Collectors.groupingBy(Task::getParentTaskId));
if (CollUtil.isEmpty(childrenTaskMap)) {
return Collections.emptyList();
}
// 3. 使用栈进行遍历,广度或深度优先搜索所有子孙任务
List<Task> result = new ArrayList<>(); List<Task> result = new ArrayList<>();
// 1. 递归获取子级
Stack<Task> stack = new Stack<>(); Stack<Task> stack = new Stack<>();
stack.push(parentTask); stack.push(parentTask);
// 2. 递归遍历
for (int i = 0; i < Short.MAX_VALUE; i++) { while (!stack.isEmpty()) {
if (stack.isEmpty()) {
break;
}
// 2.1 获取子任务们
Task task = stack.pop(); Task task = stack.pop();
List<Task> childTaskList = getTaskListByParentTaskId(task.getId()); List<Task> childTaskList = childrenTaskMap.get(task.getId());
// 2.2 如果非空,则添加到 stack 进一步递归
if (CollUtil.isNotEmpty(childTaskList)) { if (CollUtil.isNotEmpty(childTaskList)) {
stack.addAll(childTaskList); stack.addAll(childTaskList);
result.addAll(childTaskList); result.addAll(childTaskList);
@@ -659,7 +669,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
/** /**
* 审批通过存在后加签的任务。 * 审批通过存在"后加签"的任务。
* <p> * <p>
* 注意该任务不能马上完成需要一个中间状态APPROVING并激活剩余所有子任务PROCESS为可审批处理 * 注意该任务不能马上完成需要一个中间状态APPROVING并激活剩余所有子任务PROCESS为可审批处理
* 如果马上完成,则会触发下一个任务,甚至如果没有下一个任务则流程实例就直接结束了! * 如果马上完成,则会触发下一个任务,甚至如果没有下一个任务则流程实例就直接结束了!
@@ -771,7 +781,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason())); BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason()));
// 2.3 如果当前任务时被加签的,则加它的根任务也标记成未通过 // 2.3 如果当前任务时被加签的,则加它的根任务也标记成未通过
// 疑问:为什么要标记未通过呢? // 疑问:为什么要标记未通过呢?
// 回答:例如说 A 任务被向前加签除 B 任务时B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示已取消的任务,导致展示不出审批不通过的细节。 // 回答:例如说 A 任务被向前加签除 B 任务时B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示"已取消"的任务,导致展示不出审批不通过的细节。
if (task.getParentTaskId() != null) { if (task.getParentTaskId() != null) {
String rootParentId = getTaskRootParentId(task); String rootParentId = getTaskRootParentId(task);
updateTaskStatusAndReason(rootParentId, BpmTaskStatusEnum.REJECT.getStatus(), updateTaskStatusAndReason(rootParentId, BpmTaskStatusEnum.REJECT.getStatus(),
@@ -1044,8 +1054,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
/** /**
* 校验任务是否可以加签,主要校验加签类型是否一致: * 校验任务是否可以加签,主要校验加签类型是否一致:
* <p> * <p>
* 1. 如果存在向前加签的任务,则不能向后加签 * 1. 如果存在"向前加签"的任务,则不能"向后加签"
* 2. 如果存在向后加签的任务,则不能向前加签 * 2. 如果存在"向后加签"的任务,则不能"向前加签"
* *
* @param userId 当前用户 ID * @param userId 当前用户 ID
* @param reqVO 请求参数,包含任务 ID 和加签类型 * @param reqVO 请求参数,包含任务 ID 和加签类型
@@ -1366,8 +1376,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(), Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
if (userTaskElement.getId().equals(START_USER_NODE_ID) if (userTaskElement.getId().equals(START_USER_NODE_ID)
&& (skipStartUserNodeFlag == null // 目的:一般是主流程,发起人节点,自动通过审核 && (skipStartUserNodeFlag == null // 目的:一般是"主流程",发起人节点,自动通过审核
|| Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是子流程,发起人节点,按配置自动通过审核 || Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是"子流程",发起人节点,按配置自动通过审核
&& ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));

View File

@@ -142,6 +142,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
updateBusinessProduct(updateObj.getId(), businessProducts); updateBusinessProduct(updateObj.getId(), businessProducts);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldBusiness.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldBusiness, CrmBusinessSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldBusiness, CrmBusinessSaveReqVO.class));
LogRecordContext.putVariable("businessName", oldBusiness.getName()); LogRecordContext.putVariable("businessName", oldBusiness.getName());
} }

View File

@@ -92,19 +92,20 @@ public class CrmClueServiceImpl implements CrmClueService {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
success = CRM_CLUE_UPDATE_SUCCESS) success = CRM_CLUE_UPDATE_SUCCESS)
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReq.id", level = CrmPermissionLevelEnum.OWNER) @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.OWNER)
public void updateClue(CrmClueSaveReqVO updateReq) { public void updateClue(CrmClueSaveReqVO updateReqVO) {
Assert.notNull(updateReq.getId(), "线索编号不能为空"); Assert.notNull(updateReqVO.getId(), "线索编号不能为空");
// 1.1 校验线索是否存在 // 1.1 校验线索是否存在
CrmClueDO oldClue = validateClueExists(updateReq.getId()); CrmClueDO oldClue = validateClueExists(updateReqVO.getId());
// 1.2 校验关联数据 // 1.2 校验关联数据
validateRelationDataExists(updateReq); validateRelationDataExists(updateReqVO);
// 2. 更新线索 // 2. 更新线索
CrmClueDO updateObj = BeanUtils.toBean(updateReq, CrmClueDO.class); CrmClueDO updateObj = BeanUtils.toBean(updateReqVO, CrmClueDO.class);
clueMapper.updateById(updateObj); clueMapper.updateById(updateObj);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldClue.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class));
LogRecordContext.putVariable("clueName", oldClue.getName()); LogRecordContext.putVariable("clueName", oldClue.getName());
} }

View File

@@ -114,6 +114,7 @@ public class CrmContactServiceImpl implements CrmContactService {
contactMapper.updateById(updateObj); contactMapper.updateById(updateObj);
// 3. 记录操作日志 // 3. 记录操作日志
updateReqVO.setOwnerUserId(oldContact.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContact, CrmContactSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContact, CrmContactSaveReqVO.class));
LogRecordContext.putVariable("contactName", oldContact.getName()); LogRecordContext.putVariable("contactName", oldContact.getName());
} }

View File

@@ -140,9 +140,9 @@ public class CrmContractServiceImpl implements CrmContractService {
Assert.notNull(updateReqVO.getId(), "合同编号不能为空"); Assert.notNull(updateReqVO.getId(), "合同编号不能为空");
updateReqVO.setOwnerUserId(null); // 不允许更新的字段 updateReqVO.setOwnerUserId(null); // 不允许更新的字段
// 1.1 校验存在 // 1.1 校验存在
CrmContractDO contract = validateContractExists(updateReqVO.getId()); CrmContractDO oldContract = validateContractExists(updateReqVO.getId());
// 1.2 只有草稿、审批中,可以编辑; // 1.2 只有草稿、审批中,可以编辑;
if (!ObjectUtils.equalsAny(contract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), if (!ObjectUtils.equalsAny(oldContract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
CrmAuditStatusEnum.PROCESS.getStatus())) { CrmAuditStatusEnum.PROCESS.getStatus())) {
throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT); throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT);
} }
@@ -159,8 +159,9 @@ public class CrmContractServiceImpl implements CrmContractService {
updateContractProduct(updateReqVO.getId(), contractProducts); updateContractProduct(updateReqVO.getId(), contractProducts);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(contract, CrmContractSaveReqVO.class)); updateReqVO.setOwnerUserId(oldContract.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable("contractName", contract.getName()); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContract, CrmContractSaveReqVO.class));
LogRecordContext.putVariable("contractName", oldContract.getName());
} }
private void updateContractProduct(Long id, List<CrmContractProductDO> newList) { private void updateContractProduct(Long id, List<CrmContractProductDO> newList) {

View File

@@ -137,6 +137,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
customerMapper.updateById(updateObj); customerMapper.updateById(updateObj);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldCustomer.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldCustomer, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldCustomer, CrmCustomerSaveReqVO.class));
LogRecordContext.putVariable("customerName", oldCustomer.getName()); LogRecordContext.putVariable("customerName", oldCustomer.getName());
} }

View File

@@ -210,12 +210,12 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId( CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId(
transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId()); transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId());
String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType()); String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType());
if (oldPermission == null // 不是拥有者,并且不是超管 if ((oldPermission == null || !isOwner(oldPermission.getLevel()))
|| (!isOwner(oldPermission.getLevel()) && !CrmPermissionUtils.isCrmAdmin())) { && !CrmPermissionUtils.isCrmAdmin()) { // 并且不是超管
throw exception(CRM_PERMISSION_DENIED, bizTypeName); throw exception(CRM_PERMISSION_DENIED, bizTypeName);
} }
// 1.1 校验转移对象是否已经是该负责人 // 1.1 校验转移对象是否已经是该负责人
if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) { if (oldPermission != null && ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) {
throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName); throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName);
} }
// 1.2 校验新负责人是否存在 // 1.2 校验新负责人是否存在

View File

@@ -104,6 +104,7 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
receivablePlanMapper.updateById(updateObj); receivablePlanMapper.updateById(updateObj);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldReceivablePlan.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivablePlan, CrmReceivablePlanSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivablePlan, CrmReceivablePlanSaveReqVO.class));
LogRecordContext.putVariable("receivablePlan", oldReceivablePlan); LogRecordContext.putVariable("receivablePlan", oldReceivablePlan);
} }

View File

@@ -162,14 +162,14 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
Assert.notNull(updateReqVO.getId(), "回款编号不能为空"); Assert.notNull(updateReqVO.getId(), "回款编号不能为空");
updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段 updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段
// 1.1 校验存在 // 1.1 校验存在
CrmReceivableDO receivable = validateReceivableExists(updateReqVO.getId()); CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId());
updateReqVO.setOwnerUserId(receivable.getOwnerUserId()).setCustomerId(receivable.getCustomerId()) updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()).setCustomerId(oldReceivable.getCustomerId())
.setContractId(receivable.getContractId()).setPlanId(receivable.getPlanId()); // 设置已存在的值 .setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 设置已存在的值
// 1.2 校验可回款金额超过上限 // 1.2 校验可回款金额超过上限
validateReceivablePriceExceedsLimit(updateReqVO); validateReceivablePriceExceedsLimit(updateReqVO);
// 1.3 只有草稿、审批中,可以编辑; // 1.3 只有草稿、审批中,可以编辑;
if (!ObjectUtils.equalsAny(receivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
CrmAuditStatusEnum.PROCESS.getStatus())) { CrmAuditStatusEnum.PROCESS.getStatus())) {
throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED); throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);
} }
@@ -179,9 +179,10 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
receivableMapper.updateById(updateObj); receivableMapper.updateById(updateObj);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
LogRecordContext.putVariable("receivable", receivable); updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId())); LogRecordContext.putVariable("oldReceivable", oldReceivable);
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(receivable, CrmReceivableSaveReqVO.class)); LogRecordContext.putVariable("period", getReceivablePeriod(oldReceivable.getPlanId()));
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivable, CrmReceivableSaveReqVO.class));
} }
private Integer getReceivablePeriod(Long planId) { private Integer getReceivablePeriod(Long planId) {

View File

@@ -10,7 +10,9 @@
<if test="endTime != null"> <if test="endTime != null">
AND in_time &lt; #{endTime} AND in_time &lt; #{endTime}
</if> </if>
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
</if>
AND deleted = 0) - AND deleted = 0) -
(SELECT IFNULL(SUM(total_price), 0) (SELECT IFNULL(SUM(total_price), 0)
FROM erp_purchase_return FROM erp_purchase_return
@@ -18,7 +20,9 @@
<if test="endTime != null"> <if test="endTime != null">
AND return_time &lt; #{endTime} AND return_time &lt; #{endTime}
</if> </if>
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
</if>
AND deleted = 0) AND deleted = 0)
</select> </select>

View File

@@ -10,7 +10,9 @@
<if test="endTime != null"> <if test="endTime != null">
AND out_time &lt; #{endTime} AND out_time &lt; #{endTime}
</if> </if>
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
</if>
AND deleted = 0) - AND deleted = 0) -
(SELECT IFNULL(SUM(total_price), 0) (SELECT IFNULL(SUM(total_price), 0)
FROM erp_sale_return FROM erp_sale_return
@@ -18,7 +20,9 @@
<if test="endTime != null"> <if test="endTime != null">
AND return_time &lt; #{endTime} AND return_time &lt; #{endTime}
</if> </if>
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
</if>
AND deleted = 0) AND deleted = 0)
</select> </select>

View File

@@ -1,9 +1,12 @@
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 上传文件 Request VO") @Schema(description = "管理后台 - 上传文件 Request VO")
@@ -17,4 +20,10 @@ public class FileUploadReqVO {
@Schema(description = "文件目录", example = "XXX/YYY") @Schema(description = "文件目录", example = "XXX/YYY")
private String directory; private String directory;
@AssertTrue(message = "文件目录不正确")
@JsonIgnore
public boolean isDirectoryValid() {
return !StrUtil.containsAny(directory, "..", "/", "\\");
}
} }

View File

@@ -1,9 +1,12 @@
package cn.iocoder.yudao.module.infra.controller.app.file.vo; package cn.iocoder.yudao.module.infra.controller.app.file.vo;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@Schema(description = "用户 App - 上传文件 Request VO") @Schema(description = "用户 App - 上传文件 Request VO")
@@ -17,4 +20,10 @@ public class AppFileUploadReqVO {
@Schema(description = "文件目录", example = "XXX/YYY") @Schema(description = "文件目录", example = "XXX/YYY")
private String directory; private String directory;
@AssertTrue(message = "文件目录不正确")
@JsonIgnore
public boolean isDirectoryValid() {
return !StrUtil.containsAny(directory, "..", "/", "\\");
}
} }

View File

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.infra.framework.file.core.utils;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika; import org.apache.tika.Tika;
@@ -86,8 +85,8 @@ public class FileTypeUtils {
response.setContentType(contentType); response.setContentType(contentType);
// 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题
if (StrUtil.containsIgnoreCase(contentType, "video")) { if (StrUtil.containsIgnoreCase(contentType, "video")) {
response.setHeader("Content-Length", String.valueOf(content.length - 1)); response.setHeader("Content-Length", String.valueOf(content.length));
response.setHeader("Content-Range", String.valueOf(content.length - 1)); response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length);
response.setHeader("Accept-Ranges", "bytes"); response.setHeader("Accept-Ranges", "bytes");
} }
// 输出附件 // 输出附件

View File

@@ -31,9 +31,13 @@ import java.util.List;
public class CouponTemplateDO extends BaseDO { public class CouponTemplateDO extends BaseDO {
/** /**
* 不限制领取数量 * 领取数量 - 不限制
*/ */
public static final Integer TIME_LIMIT_COUNT_MAX = -1; public static final Integer TAKE_LIMIT_COUNT_MAX = -1;
/**
* 发放数量 - 不限制
*/
public static final Integer TOTAL_COUNT_MAX = -1;
// ========== 基本信息 BEGIN ========== // ========== 基本信息 BEGIN ==========
/** /**

View File

@@ -40,10 +40,16 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
.orderByDesc(CouponTemplateDO::getId)); .orderByDesc(CouponTemplateDO::getId));
} }
default void updateTakeCount(Long id, Integer incrCount) { default int updateTakeCount(Long id, Integer incrCount) {
update(null, new LambdaUpdateWrapper<CouponTemplateDO>() LambdaUpdateWrapper<CouponTemplateDO> updateWrapper = new LambdaUpdateWrapper<CouponTemplateDO>()
.eq(CouponTemplateDO::getId, id) .eq(CouponTemplateDO::getId, id)
.setSql("take_count = take_count + " + incrCount)); .setSql("take_count = take_count + " + incrCount);
// 增加已领取的数量incrCount 为正数),需要考虑发放数量 totalCount 的限制
if (incrCount > 0) {
updateWrapper.and(i -> i.apply("take_count < total_count")
.or().eq(CouponTemplateDO::getTotalCount, CouponTemplateDO.TOTAL_COUNT_MAX));
}
return update(updateWrapper);
} }
default List<CouponTemplateDO> selectListByTakeType(Integer takeType) { default List<CouponTemplateDO> selectListByTakeType(Integer takeType) {

View File

@@ -86,7 +86,7 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
activityList.removeIf(item -> ObjectUtil.equal(item.getId(), activityId)); activityList.removeIf(item -> ObjectUtil.equal(item.getId(), activityId));
} }
// 查找是否有其它活动,选择了该产品 // 查找是否有其它活动,选择了该产品
List<CombinationActivityDO> matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getId(), spuId)); List<CombinationActivityDO> matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getSpuId(), spuId));
if (CollUtil.isNotEmpty(matchActivityList)) { if (CollUtil.isNotEmpty(matchActivityList)) {
throw exception(COMBINATION_ACTIVITY_SPU_CONFLICTS); throw exception(COMBINATION_ACTIVITY_SPU_CONFLICTS);
} }

View File

@@ -137,7 +137,6 @@ public class CouponServiceImpl implements CouponService {
// 4. 增加优惠劵模板的领取数量 // 4. 增加优惠劵模板的领取数量
couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size()); couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size());
return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId);
} }
@@ -281,7 +280,7 @@ public class CouponServiceImpl implements CouponService {
} }
// 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制 && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制
&& couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) { && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
throw exception(COUPON_TEMPLATE_NOT_ENOUGH); throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
} }

View File

@@ -22,8 +22,7 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL;
/** /**
* 优惠劵模板 Service 实现类 * 优惠劵模板 Service 实现类
@@ -60,7 +59,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId()); CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId());
// 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制 && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制
&& updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) { && updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) {
throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount()); throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount());
} }
@@ -116,7 +115,10 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
@Override @Override
public void updateCouponTemplateTakeCount(Long id, int incrCount) { public void updateCouponTemplateTakeCount(Long id, int incrCount) {
couponTemplateMapper.updateTakeCount(id, incrCount); int updateCount = couponTemplateMapper.updateTakeCount(id, incrCount);
if (updateCount == 0) {
throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
}
} }
@Override @Override

View File

@@ -76,8 +76,8 @@
<select id="selectListByPayTimeBetweenAndGroupByMonth" <select id="selectListByPayTimeBetweenAndGroupByMonth"
resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderTrendRespVO"> resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderTrendRespVO">
SELECT DATE_FORMAT(pay_time, '%Y-%m') AS date, SELECT DATE_FORMAT(pay_time, '%Y-%m') AS date,
COUNT(1) AS orderPayCount, COUNT(1) AS order_pay_count,
SUM(pay_price) AS orderPayPrice SUM(pay_price) AS order_pay_price
FROM trade_order FROM trade_order
WHERE pay_status = TRUE WHERE pay_status = TRUE
AND create_time BETWEEN #{beginTime} AND #{endTime} AND create_time BETWEEN #{beginTime} AND #{endTime}
@@ -95,8 +95,8 @@
<select id="selectPaySummaryByPayStatusAndPayTimeBetween" <select id="selectPaySummaryByPayStatusAndPayTimeBetween"
resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderSummaryRespVO"> resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderSummaryRespVO">
SELECT IFNULL(SUM(pay_price), 0) AS orderPayPrice, SELECT IFNULL(SUM(pay_price), 0) AS order_pay_price,
COUNT(1) AS orderPayCount COUNT(1) AS order_pay_count
FROM trade_order FROM trade_order
WHERE pay_status = #{payStatus} WHERE pay_status = #{payStatus}
AND pay_time BETWEEN #{beginTime} AND #{endTime} AND pay_time BETWEEN #{beginTime} AND #{endTime}

View File

@@ -39,7 +39,8 @@ public interface ErrorCodeConstants {
ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_034, "交易订单更新支付订单退款状态失败,原因:退款单不存在"); ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_034, "交易订单更新支付订单退款状态失败,原因:退款单不存在");
ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_035, "交易订单更新支付订单退款状态失败,原因:退款单状态不是【退款成功】"); ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_035, "交易订单更新支付订单退款状态失败,原因:退款单状态不是【退款成功】");
ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_036, "交易订单自提失败,原因:你没有核销该门店订单的权限"); ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_036, "交易订单自提失败,原因:你没有核销该门店订单的权限");
ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_037, "交易订单创建失败,原因:用户积分不足"); ErrorCode ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS = new ErrorCode(1_011_000_037, "交易订单自提失败,原因:商品拼团记录不是【成功】状态");
ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_038, "交易订单创建失败,原因:用户积分不足");
// ========== After Sale 模块 1-011-000-100 ========== // ========== After Sale 模块 1-011-000-100 ==========
ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在"); ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");

View File

@@ -141,9 +141,8 @@ public class AfterSaleController {
public CommonResult<Boolean> updateAfterSaleRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { public CommonResult<Boolean> updateAfterSaleRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO); log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO);
if (StrUtil.startWithAny(notifyReqDTO.getMerchantRefundId(), "order-")) { if (StrUtil.startWithAny(notifyReqDTO.getMerchantRefundId(), "order-")) {
tradeOrderUpdateService.updatePaidOrderRefunded( Long orderId = Long.parseLong(StrUtil.subAfter(notifyReqDTO.getMerchantRefundId(), "order-", true));
Long.parseLong(notifyReqDTO.getMerchantRefundId()), tradeOrderUpdateService.updatePaidOrderRefunded(orderId, notifyReqDTO.getPayRefundId());
notifyReqDTO.getPayRefundId());
} else { } else {
afterSaleService.updateAfterSaleRefunded( afterSaleService.updateAfterSaleRefunded(
Long.parseLong(notifyReqDTO.getMerchantRefundId()), Long.parseLong(notifyReqDTO.getMerchantRefundId()),

View File

@@ -25,6 +25,9 @@ import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
import cn.iocoder.yudao.module.system.api.social.SocialClientApi; import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
@@ -121,6 +124,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
public SocialClientApi socialClientApi; public SocialClientApi socialClientApi;
@Resource @Resource
public PayRefundApi payRefundApi; public PayRefundApi payRefundApi;
@Resource
private CombinationRecordApi combinationRecordApi;
@Resource @Resource
private TradeOrderProperties tradeOrderProperties; private TradeOrderProperties tradeOrderProperties;
@@ -775,6 +780,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
if (ObjUtil.notEqual(DeliveryTypeEnum.PICK_UP.getType(), order.getDeliveryType())) { if (ObjUtil.notEqual(DeliveryTypeEnum.PICK_UP.getType(), order.getDeliveryType())) {
throw exception(ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP); throw exception(ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP);
} }
// 情况一:如果是拼团订单,则校验拼团是否成功
if (TradeOrderTypeEnum.isCombination(order.getType())) {
CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId(
order.getUserId(), order.getId());
if (!CombinationRecordStatusEnum.isSuccess(combinationRecord.getStatus())) {
throw exception(ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS);
}
}
DeliveryPickUpStoreDO deliveryPickUpStore = pickUpStoreService.getDeliveryPickUpStore(order.getPickUpStoreId()); DeliveryPickUpStoreDO deliveryPickUpStore = pickUpStoreService.getDeliveryPickUpStore(order.getPickUpStoreId());
if (deliveryPickUpStore == null if (deliveryPickUpStore == null
|| !CollUtil.contains(deliveryPickUpStore.getVerifyUserIds(), userId)) { || !CollUtil.contains(deliveryPickUpStore.getVerifyUserIds(), userId)) {

View File

@@ -1,10 +1,12 @@
package cn.iocoder.yudao.module.mp.service.handler.user; package cn.iocoder.yudao.module.mp.service.handler.user;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder;
import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService; import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService;
import cn.iocoder.yudao.module.mp.service.user.MpUserService; import cn.iocoder.yudao.module.mp.service.user.MpUserService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxMpErrorMsgEnum;
import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpMessageHandler;
import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.api.WxMpService;
@@ -40,6 +42,13 @@ public class SubscribeHandler implements WxMpMessageHandler {
wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser()); wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser());
} catch (WxErrorException e) { } catch (WxErrorException e) {
log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e); log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e);
// 特殊情况个人账号无接口权限https://t.zsxq.com/cLFq5
if (ObjUtil.equal(e.getError().getErrorCode(), WxMpErrorMsgEnum.CODE_48001)) {
wxMpUser = new WxMpUser();
wxMpUser.setOpenId(wxMessage.getFromUser());
wxMpUser.setSubscribe(true);
wxMpUser.setSubscribeTime(System.currentTimeMillis() / 1000L);
}
} }
// 第二步,保存粉丝信息 // 第二步,保存粉丝信息

View File

@@ -127,6 +127,8 @@ public class AuthController {
@PostMapping("/sms-login") @PostMapping("/sms-login")
@PermitAll @PermitAll
@Operation(summary = "使用短信验证码登录") @Operation(summary = "使用短信验证码登录")
// 可按需开启限流https://github.com/YunaiV/ruoyi-vue-pro/issues/851
// @RateLimiter(time = 60, count = 6, keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#reqVO.mobile")
public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) {
return success(authService.smsLogin(reqVO)); return success(authService.smsLogin(reqVO));
} }

View File

@@ -38,7 +38,8 @@ public class SocialUserController {
@PostMapping("/bind") @PostMapping("/bind")
@Operation(summary = "社交绑定,使用 code 授权码") @Operation(summary = "社交绑定,使用 code 授权码")
public CommonResult<Boolean> socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) { public CommonResult<Boolean> socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) {
socialUserService.bindSocialUser(BeanUtils.toBean(reqVO, SocialUserBindReqDTO.class) socialUserService.bindSocialUser(new SocialUserBindReqDTO().setSocialType(reqVO.getType())
.setCode(reqVO.getCode()).setState(reqVO.getState())
.setUserId(getLoginUserId()).setUserType(UserTypeEnum.ADMIN.getValue())); .setUserId(getLoginUserId()).setUserType(UserTypeEnum.ADMIN.getValue()));
return CommonResult.success(true); return CommonResult.success(true);
} }

View File

@@ -6,8 +6,8 @@ server:
spring: spring:
autoconfigure: autoconfigure:
exclude: exclude:
- org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建 - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建
- org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建 - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建
# 数据源配置项 # 数据源配置项
autoconfigure: autoconfigure:
exclude: exclude:

View File

@@ -11,8 +11,8 @@ spring:
- de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置 - de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置
- de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置 - de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置
- de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置
- org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建 - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建
- org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建 - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建
# 数据源配置项 # 数据源配置项
datasource: datasource:
druid: # Druid 【监控】相关的全局配置 druid: # Druid 【监控】相关的全局配置

View File

@@ -150,7 +150,7 @@ spring:
vectorstore: # 向量存储 vectorstore: # 向量存储
redis: redis:
initialize-schema: true initialize-schema: true
index: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 index-name: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行
prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构 prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构
qdrant: qdrant:
initialize-schema: true initialize-schema: true
@@ -188,13 +188,14 @@ spring:
api-key: xxxx api-key: xxxx
moonshot: # 月之暗灭KIMI moonshot: # 月之暗灭KIMI
api-key: sk-abc api-key: sk-abc
deepseek: # DeepSeek
api-key: sk-e94db327cc7d457d99a8de8810fc6b12
chat:
options:
model: deepseek-chat
yudao: yudao:
ai: ai:
deep-seek: # DeepSeek
enable: true
api-key: sk-e94db327cc7d457d99a8de8810fc6b12
model: deepseek-chat
doubao: # 字节豆包 doubao: # 字节豆包
enable: true enable: true
api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272