Compare commits

..

30 Commits

Author SHA1 Message Date
3e107d766e 注解缓存sa-token的@SaCheckRole( 2025-12-09 12:06:41 +08:00
cac4121209 合并升级3.9.0 2025-12-09 11:45:38 +08:00
3599120c94 注解使用错误 2025-11-13 09:51:06 +08:00
cf9b407a18 Merge remote-tracking branch 'origin/springboot3' into springboot3-satoken 2025-11-10 15:58:23 +08:00
a194d4e9b2 Merge remote-tracking branch 'origin/springboot3' into springboot3-satoken 2025-10-28 23:00:18 +08:00
21585e4d25 Merge remote-tracking branch 'origin/springboot3' into springboot3-satoken
# Conflicts:
#	.gitignore
2025-10-22 09:38:00 +08:00
0489d30296 更新README.md,修改SpringBoot版本信息 2025-10-17 19:29:18 +08:00
ed87ac3bff 更新租户ID设置的注释,提升代码可读性 2025-10-16 23:08:16 +08:00
761dbf0343 【sa-token】多租户的check和线程会话设置 2025-10-16 23:05:33 +08:00
23c628057b 重命名迁移说明文件并调整标题格式 2025-10-16 19:31:21 +08:00
2ac14709ba 兼容shiro获取用户API写法,适配online底层lib依赖 2025-10-16 19:23:35 +08:00
f9cff08716 更新README.md,修正Sa-Token下载链接格式 2025-10-16 19:14:58 +08:00
a6feb2fd9d 更新README.md,调整下载链接顺序 2025-10-16 19:11:02 +08:00
b84eb25d41 新版说明 2025-10-16 19:03:20 +08:00
4326cecad4 【sa-token】仪表盘静态资源需要排除权限拦截 2025-10-16 16:30:36 +08:00
ec5810176b 1 2025-10-16 15:06:37 +08:00
aff307c3ff 【sa-token】支持异步请求SseEmitter 2025-10-16 14:50:30 +08:00
acfd3bb3e4 1 2025-10-16 14:26:44 +08:00
52082fb256 【sa-token】自动续期不好使,需要手动执行续期方法StpUtil.stpLogic.updateLastActiveToNow(token); 2025-10-16 13:45:17 +08:00
736515f63a 【sa-token】忽略权限注解不好使 2025-10-16 12:36:29 +08:00
a250163198 【权限替换为sa-token】更新迁移文档 2025-10-16 11:50:46 +08:00
1ed1f315a4 【权限替换为sa-token】更新迁移文档 2025-10-16 11:44:55 +08:00
f7670dca3a 【权限替换为sa-token】优化退出登录日志 2025-10-16 11:41:12 +08:00
b24ac544c8 【权限替换为sa-token】token无效异常提醒修改 2025-10-16 11:40:53 +08:00
c7c31e0945 【sa-token】shiro替换为sa-token,核心架构修改点 2025-10-16 11:14:53 +08:00
468af57489 【sa-token】登录和退出换写法 2025-10-16 11:06:47 +08:00
c85bb1f62d 【sa-token】替换权限注解和权限缓存处理 2025-10-16 11:05:33 +08:00
b4fa11a605 【sa-token】获取用户信息和校验token有效的API变更 2025-10-16 10:42:44 +08:00
b2240848e0 删除无用代码 2025-10-15 21:23:15 +08:00
4a888a4e19 【权限框架换成sa-token】替换sa-token权限注解和替换获取用户工具类LoginUserUtils 2025-10-15 21:21:07 +08:00
422 changed files with 114855 additions and 51103 deletions

View File

@ -106,10 +106,6 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
| ChatGTP | √ | | ChatGTP | √ |
| Qwq | √ | | Qwq | √ |
| 智库 | √ | | 智库 | √ |
| claude | √ |
| vl模型 | √ |
| 千帆大模型 | √ |
| 通义千问 | √ |
| Ollama本地搭建大模型 | √ | | Ollama本地搭建大模型 | √ |
| 等等。。 | √ | | 等等。。 | √ |

View File

@ -1,4 +1,4 @@
[中文](./README.md) | English
![JEECG](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/logov3.png "JeecgBoot低代码开发平台") ![JEECG](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/logov3.png "JeecgBoot低代码开发平台")
@ -7,12 +7,12 @@
JEECG BOOT AI Low Code Platform JEECG BOOT AI Low Code Platform
=============== ===============
Current version: 3.9.1 (Release date: 2026-01-22) Current version: 3.9.0 (Release date: 2025-12-01)
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE) [![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-guojusoft-orange.svg)](http://www.jeecg.com) [![](https://img.shields.io/badge/Author-guojusoft-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/version-3.9.1-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot) [![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot) [![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot) [![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)

View File

@ -1,15 +1,14 @@
中文 | [English](./README.en-US.md)
JeecgBoot AI低代码平台 JeecgBoot AI低代码平台
=============== ===============
当前最新版本: 3.9.1发布日期2026-01-22 当前最新版本: 3.9.0发布日期2025-12-01
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE) [![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](https://jeecg.com) [![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](https://jeecg.com)
[![](https://img.shields.io/badge/blog-技术博客-orange.svg)](https://jeecg.blog.csdn.net) [![](https://img.shields.io/badge/blog-技术博客-orange.svg)](https://jeecg.blog.csdn.net)
[![](https://img.shields.io/badge/version-3.9.1-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot) [![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/jeecgboot/JeecgBoot) [![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/jeecgboot/JeecgBoot) [![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/jeecgboot/JeecgBoot)

View File

@ -1,12 +1,13 @@
JeecgBoot 低代码开发平台 JeecgBoot 低代码开发平台
=============== ===============
当前最新版本: 3.9.1(发布日期: 2026-01-22 当前最新版本: 3.9.0发布日期2025-12-01
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE) [![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex) [![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/version-3.9.1-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot) [![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot) [![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot) [![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)

View File

@ -0,0 +1,368 @@
# `Shiro 到 Sa-Token 迁移指南`
本项目已从 **Apache Shiro 2.0.4** 迁移到 **Sa-Token 1.44.0**,采用 JWT-Simple 模式,完全兼容原 JWT token 格式。
---
## 📦 1. 依赖配置
### 1.1 Maven 依赖
移除 Shiro 相关依赖,新增:
```xml
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.44.0</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.44.0</version>
</dependency>
```
### 1.2 配置文件application.yml
```yaml
sa-token:
token-name: X-Access-Token
timeout: 2592000 # token有效期30天
is-concurrent: true # 允许同账号并发登录
token-style: jwt-simple # JWT模式兼容原格式
jwt-secret-key: "your-secret-key-here"
```
---
## 💡 2. 核心代码实现
### 2.1 登录逻辑(⚠️ 使用 username 作为 loginId
```java
// 从数据库查询用户信息
SysUser sysUser = userService.getUserByUsername(username);
// 执行登录自动完成Sa-Token登录 + 存储Session + 返回token
String token = LoginUserUtils.doLogin(sysUser);
// 返回token给前端
return Result.ok(token);
```
**💡 设计说明:**
- `doLogin()` 方法自动完成:
1. 调用 `StpUtil.login(username)` (使用 username 而非 userId
2. 调用 `setSessionUser()` 存储用户信息(自动清除 password 等15个字段
3. 返回生成的 token
- 减少 Redis 存储约 50%,密码不再存储到 Session
### 2.2 权限认证接口(⚠️ 必须手动实现缓存)
```java
@Component
public class StpInterfaceImpl implements StpInterface {
@Lazy @Resource
private CommonAPI commonApi;
private static final long CACHE_TIMEOUT = 60 * 60 * 24 * 30; // 30天
private static final String PERMISSION_CACHE_PREFIX = "satoken:user-permission:";
private static final String ROLE_CACHE_PREFIX = "satoken:user-role:";
@Override
@SuppressWarnings("unchecked")
public List<String> getPermissionList(Object loginId, String loginType) {
String username = loginId.toString();
String cacheKey = PERMISSION_CACHE_PREFIX + username;
SaTokenDao dao = SaManager.getSaTokenDao();
// 1. 先从缓存获取
List<String> permissionList = (List<String>) dao.getObject(cacheKey);
if (permissionList == null) {
// 2. 缓存未命中,查询数据库
log.warn("权限缓存未命中,查询数据库 [ username={} ]", username);
String userId = commonApi.getUserIdByName(username);
Set<String> permissionSet = commonApi.queryUserAuths(userId);
permissionList = new ArrayList<>(permissionSet);
// 3. 将结果缓存起来
dao.setObject(cacheKey, permissionList, CACHE_TIMEOUT);
}
return permissionList;
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 实现类似 getPermissionList(),使用 ROLE_CACHE_PREFIX
// 详见StpInterfaceImpl.java
}
// 清除缓存的静态方法
public static void clearUserCache(List<String> usernameList) {
SaTokenDao dao = SaManager.getSaTokenDao();
for (String username : usernameList) {
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
dao.deleteObject(ROLE_CACHE_PREFIX + username);
}
}
}
```
**⚠️ 关键:** Sa-Token 的 `StpInterface` **不提供自动缓存**,必须手动实现,否则每次请求都会查询数据库!
### 2.3 Filter 配置(支持 URL 参数传递 token
```java
@Bean
@Primary
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple() {
@Override
public String getTokenValue() {
SaRequest request = SaHolder.getRequest();
// 优先级Header > URL参数"token" > URL参数"X-Access-Token"
String tokenValue = request.getHeader(getConfigOrGlobal().getTokenName());
if (isEmpty(tokenValue)) {
tokenValue = request.getParam("token"); // 兼容 WebSocket、积木报表
}
if (isEmpty(tokenValue)) {
tokenValue = request.getParam(getConfigOrGlobal().getTokenName());
}
return isEmpty(tokenValue) ? super.getTokenValue() : tokenValue;
}
};
}
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.setExcludeList(getExcludeUrls()) // 排除登录、静态资源等
.setAuth(obj -> {
// 检查是否是免认证路径
String servletPath = SaHolder.getRequest().getRequestPath();
if (InMemoryIgnoreAuth.contains(servletPath)) {
return;
}
// ⚠️ 关键:如果请求带 token先切换到对应的登录会话
try {
String token = StpUtil.getTokenValue();
if (isNotEmpty(token)) {
Object loginId = StpUtil.getLoginIdByToken(token);
if (loginId != null) {
StpUtil.switchTo(loginId); // 切换登录会话
}
}
} catch (Exception e) {
log.debug("切换登录会话失败: {}", e.getMessage());
}
// 最终校验登录状态
StpUtil.checkLogin();
})
.setError(e -> {
// 返回401 JSON响应
SaHolder.getResponse()
.setStatus(401)
.setHeader("Content-Type", "application/json;charset=UTF-8");
return JwtUtil.responseErrorJson(401, "Token失效请重新登录!");
});
}
```
### 2.4 全局异常处理
```java
@ExceptionHandler(NotLoginException.class)
public Result<?> handleNotLoginException(NotLoginException e) {
log.warn("用户未登录或Token失效: {}", e.getMessage());
return Result.error(401, "Token失效请重新登录!");
}
@ExceptionHandler(NotPermissionException.class)
public Result<?> handleNotPermissionException(NotPermissionException e) {
log.warn("权限不足: {}", e.getMessage());
return Result.error(403, "用户权限不足,无法访问!");
}
```
---
## 🔄 3. API 迁移对照表
### 3.1 注解替换
| Shiro | Sa-Token | 说明 |
|-------|----------|------|
| `@RequiresPermissions("user:add")` | `@SaCheckPermission("user:add")` | 权限校验 |
| `@RequiresRoles("admin")` | `@SaCheckRole("admin")` | 角色校验 |
### 3.2 API 替换
| Shiro | Sa-Token | 说明 |
|-------|----------|------|
| `SecurityUtils.getSubject().getPrincipal()` | `LoginUserUtils.getSessionUser()` | 获取登录用户 |
| `Subject.login(token)` | `LoginUserUtils.doLogin(sysUser)` | 登录(推荐) |
| `Subject.login(token)` | `StpUtil.login(username)` | 登录底层API |
| `Subject.logout()` | `StpUtil.logout()` | 退出登录 |
| `Subject.isAuthenticated()` | `StpUtil.isLogin()` | 判断是否登录 |
| `Subject.hasRole("admin")` | `StpUtil.hasRole("admin")` | 判断角色 |
| `Subject.isPermitted("user:add")` | `StpUtil.hasPermission("user:add")` | 判断权限 |
---
## ⚠️ 4. 重要特性说明
### 4.1 JWT-Simple 模式特性
-**生成标准 JWT token**:与原 Shiro JWT 格式完全兼容
-**仍然检查 Redis Session**:支持强制退出(与纯 JWT 无状态模式不同)
-**支持 URL 参数传递**:兼容 WebSocket、积木报表等场景
- ⚠️ **非完全无状态**:依赖 Redis 存储会话和权限缓存
### 4.2 Session 数据优化
`LoginUserUtils.setSessionUser()` 会自动清除以下字段:
```
password, workNo, birthday, sex, email, phone, status,
delFlag, activitiSync, createTime, userIdentity, post,
telephone, clientId, mainDepPostId
```
**优势:**
- 减少 Redis 存储约 **50%**
- 密码不再存储在 Session 中,**安全性提升**
### 4.3 权限缓存动态更新
修改角色权限后,系统会自动清除受影响用户的权限缓存:
```java
// SysPermissionController.saveRolePermission() 中
@RequestMapping(value = "/saveRolePermission", method = RequestMethod.POST)
public Result<String> saveRolePermission(@RequestBody JSONObject json) {
String roleId = json.getString("roleId");
String permissionIds = json.getString("permissionIds");
String lastPermissionIds = json.getString("lastpermissionIds");
// 保存角色权限关系
sysRolePermissionService.saveRolePermission(roleId, permissionIds, lastPermissionIds);
// ⚠️ 关键:清除拥有该角色的所有用户的权限缓存
clearRolePermissionCache(roleId);
return Result.ok("保存成功!");
}
// 实现:查询该角色下的所有用户,批量清除缓存
private void clearRolePermissionCache(String roleId) {
List<String> usernameList = new ArrayList<>();
// 分页查询拥有该角色的用户
int pageNo = 1, pageSize = 100;
while (true) {
Page<SysUser> page = new Page<>(pageNo, pageSize);
IPage<SysUser> userPage = sysUserService.getUserByRoleId(page, roleId, null, null);
if (userPage.getRecords().isEmpty()) break;
for (SysUser user : userPage.getRecords()) {
usernameList.add(user.getUsername());
}
if (pageNo >= userPage.getPages()) break;
pageNo++;
}
// 批量清除用户权限和角色缓存
if (!usernameList.isEmpty()) {
StpInterfaceImpl.clearUserCache(usernameList);
}
}
```
**结果:** 权限变更立即生效,用户无需重新登录。
## ✅ 6. 测试清单
### 6.1 登录功能测试
| 测试项 | 测试状态 | 说明 |
|--------|---------|------|
| 账号密码登录 | ✅ 通过 | 验证 `/sys/login` 接口 |
| 手机号登录 | ✅ 通过 | 验证 `/sys/phoneLogin` 接口 |
| APP 登录 | ✅ 通过 | 验证 APP 端登录流程 |
| 扫码登录 | ✅ 通过 | 验证二维码扫码登录 |
| 第三方登录 | ⏳ 待测试 | 微信、QQ 等第三方登录 |
| 钉钉 OAuth2.0 登录 | ⏳ 待测试 | 钉钉授权登录流程 |
| 企业微信 OAuth2.0 登录 | ⏳ 待测试 | 企业微信授权登录流程 |
| CAS 单点登录 | ⏳ 待测试 | CAS 单点登录集成 |
### 6.2 核心功能测试
| 测试项 | 测试状态 | 说明 |
|--------|---------|------|
| Token 权限拦截 | ✅ 通过 | 无 token 或失效 token 返回 401 |
| 权限注解 `@SaCheckPermission` | ✅ 通过 | 无权限返回 403 |
| 角色注解 `@SaCheckRole` | ✅ 通过 | 无角色返回 403 |
| `@IgnoreAuth` 免认证 | ✅ 通过 | 无 token 也能正常访问 |
| 自动续期(操作不掉线) | ✅ 通过 | 活跃用户 token 自动续期 |
| 用户权限变更即刻生效 | ✅ 通过 | 修改角色权限后无需重新登录 |
| 积木报表 token 参数模式 | ✅ 通过 | `/jmreport/**?token=xxx` 正常访问 |
### 6.3 异步和网关测试
| 测试项 | 测试状态 | 说明 |
|--------|---------|------|
| 异步接口(`@Async` | ❌ 有问题 | **需排查:异步线程中获取登录用户失败** |
| Gateway 模式权限验证 | ⏳ 待测试 | 网关模式下的权限拦截 |
### 6.4 多租户测试
| 测试项 | 测试状态 | 说明 |
|--------|---------|------|
| 租户 ID 校验 | ⚠️ 缺失 | **需补充:校验用户 tenant_id 和前端传参一致性** |
### 6.5 测试说明
**✅ 通过** - 功能正常,符合预期
**❌ 有问题** - 功能异常,需要修复
**⏳ 待测试** - 尚未测试
**⚠️ 缺失** - 功能缺失,需要补充
---
## 📊 7. 迁移总结
| 优化项 | 说明 | 收益 |
|--------|------|------|
| **loginId 设计** | 使用 `username` 而非 `userId` | 语义清晰,与业务逻辑一致 |
| **Session 优化** | 清除 15 个不必要字段 | Redis 存储减少 50%,安全性提升 |
| **权限缓存** | 手动实现 30 天缓存 | 性能提升 99%,降低 DB 压力 |
| **权限实时更新** | 角色权限修改后自动清除缓存 | 无需重新登录即生效 |
| **URL Token 支持** | Filter 中实现 `switchTo` | 兼容 WebSocket、积木报表等场景 |
| **JWT 兼容** | JWT-Simple 模式 | 完全兼容原 JWT token 格式 |
---
## 📚 参考资料
- [Sa-Token 官方文档](https://sa-token.cc/)
- [Sa-Token JWT-Simple 模式](https://sa-token.cc/doc.html#/plugin/jwt-extend)
- [Sa-Token 权限缓存最佳实践](https://sa-token.cc/doc.html#/fun/jur-cache)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.jeecgframework.boot3</groupId> <groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-parent</artifactId> <artifactId>jeecg-boot-parent</artifactId>
<version>3.9.1</version> <version>3.9.0</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId> <artifactId>jeecg-boot-base-core</artifactId>
@ -185,83 +185,23 @@
<artifactId>spring-boot-starter-quartz</artifactId> <artifactId>spring-boot-starter-quartz</artifactId>
</dependency> </dependency>
<!--JWT--> <!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency> <dependency>
<groupId>com.auth0</groupId> <groupId>cn.dev33</groupId>
<artifactId>java-jwt</artifactId> <artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${java-jwt.version}</version> <version>${sa-token.version}</version>
</dependency> </dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<!--shiro-->
<dependency> <dependency>
<groupId>org.apache.shiro</groupId> <groupId>cn.dev33</groupId>
<artifactId>shiro-spring-boot-starter</artifactId> <artifactId>sa-token-redis-jackson</artifactId>
<classifier>jakarta</classifier> <version>${sa-token.version}</version>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<!-- Sa-Token 整合 jwt (Simple模式)保持与原JWT token格式兼容 -->
<dependency> <dependency>
<groupId>org.apache.shiro</groupId> <groupId>cn.dev33</groupId>
<artifactId>shiro-spring</artifactId> <artifactId>sa-token-jwt</artifactId>
<classifier>jakarta</classifier> <version>${sa-token.version}</version>
<version>${shiro.version}</version>
<!-- 排除仍使用了javax.servlet的依赖 -->
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入适配jakarta的依赖包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<artifactId>checkstyle</artifactId>
<groupId>com.puppycrawl.tools</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
@ -305,7 +245,7 @@
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<!-- minio文件存储服务 --> <!-- mini文件存储服务 -->
<dependency> <dependency>
<groupId>io.minio</groupId> <groupId>io.minio</groupId>
<artifactId>minio</artifactId> <artifactId>minio</artifactId>

View File

@ -0,0 +1,21 @@
package org.apache.shiro;
import org.apache.shiro.subject.Subject;
/**
* 兼容处理Online功能使用处理请勿修改
* @author eightmonth@qq.com
* @date 2024/4/29 14:05
*/
public class SecurityUtils {
public static Subject getSubject() {
return new Subject() {
@Override
public Object getPrincipal() {
return Subject.super.getPrincipal();
}
};
}
}

View File

@ -0,0 +1,15 @@
package org.apache.shiro.subject;
import org.jeecg.common.util.LoginUserUtils;
/**
* 兼容处理Online功能使用处理请勿修改
* @author eightmonth@qq.com
* @date 2024/4/29 14:18
*/
public interface Subject {
default Object getPrincipal() {
return LoginUserUtils.getSessionUser();
}
}

View File

@ -33,10 +33,4 @@ public class AiragFlowDTO implements Serializable {
* 输入参数 * 输入参数
*/ */
private Map<String, Object> inputParams; private Map<String, Object> inputParams;
/**
* 是否流式返回
*/
private boolean isStream;
} }

View File

@ -2,7 +2,7 @@ package org.jeecg.common.aspect;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.PropertyFilter; import com.alibaba.fastjson.serializer.PropertyFilter;
import org.apache.shiro.SecurityUtils; import org.jeecg.common.util.LoginUserUtils;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
@ -100,7 +100,7 @@ public class AutoLogAspect {
//设置IP地址 //设置IP地址
dto.setIp(IpUtils.getIpAddr(request)); dto.setIp(IpUtils.getIpAddr(request));
//获取登录用户信息 //获取登录用户信息
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); LoginUser sysUser = LoginUserUtils.getSessionUser();
if(sysUser!=null){ if(sysUser!=null){
dto.setUserid(sysUser.getUsername()); dto.setUserid(sysUser.getUsername());
dto.setUsername(sysUser.getRealname()); dto.setUsername(sysUser.getRealname());
@ -243,7 +243,7 @@ public class AutoLogAspect {
sysLog.setIp(IPUtils.getIpAddr(request)); sysLog.setIp(IPUtils.getIpAddr(request));
//获取登录用户信息 //获取登录用户信息
LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); LoginUser sysUser = LoginUserUtils.getLoginUser();
if(sysUser!=null){ if(sysUser!=null){
sysLog.setUserid(sysUser.getUsername()); sysLog.setUserid(sysUser.getUsername());
sysLog.setUsername(sysUser.getRealname()); sysLog.setUsername(sysUser.getRealname());

View File

@ -87,30 +87,6 @@ public interface CommonConstant {
/**访问权限认证未通过 510*/ /**访问权限认证未通过 510*/
Integer SC_JEECG_NO_AUTHZ=510; Integer SC_JEECG_NO_AUTHZ=510;
/** 登录用户Shiro权限缓存KEY前缀 */
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
/** 登录用户Token令牌缓存KEY前缀 */
String PREFIX_USER_TOKEN = "prefix_user_token:";
/** 登录用户Token令牌作废提示信息比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
String PREFIX_USER_TOKEN_ERROR_MSG = "prefix_user_token:error:msg_";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
/** 客户端类型PC端 */
String CLIENT_TYPE_PC = "PC";
/** 客户端类型APP端 */
String CLIENT_TYPE_APP = "APP";
/** 客户端类型:手机号登录 */
String CLIENT_TYPE_PHONE = "PHONE";
String PREFIX_USER_TOKEN_PC = "prefix_user_token:single_login:pc:";
/** 单点登录用户在APP端的Token缓存KEY前缀 (username -> token) */
String PREFIX_USER_TOKEN_APP = "prefix_user_token:single_login:app:";
/** 单点登录用户在手机号登录的Token缓存KEY前缀 (username -> token) */
String PREFIX_USER_TOKEN_PHONE = "prefix_user_token:single_login:phone:";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
// /** Token缓存时间3600秒即一小时 */
// int TOKEN_EXPIRE_TIME = 3600;
/** 登录二维码 */ /** 登录二维码 */
String LOGIN_QRCODE_PRE = "QRCODELOGIN:"; String LOGIN_QRCODE_PRE = "QRCODELOGIN:";
String LOGIN_QRCODE = "LQ:"; String LOGIN_QRCODE = "LQ:";
@ -741,4 +717,13 @@ public interface CommonConstant {
* 发送短信方式:阿里云 * 发送短信方式:阿里云
*/ */
String SMS_SEND_TYPE_ALI_YUN = "aliyun"; String SMS_SEND_TYPE_ALI_YUN = "aliyun";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
/** 客户端类型PC端 */
String CLIENT_TYPE_PC = "PC";
/** 客户端类型APP端 */
String CLIENT_TYPE_APP = "APP";
/** 客户端类型:手机号登录 */
String CLIENT_TYPE_PHONE = "PHONE";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
} }

View File

@ -47,8 +47,4 @@ public interface TenantConstant {
*/ */
String APP_ADMIN = "appAdmin"; String APP_ADMIN = "appAdmin";
/**
* 增加SignatureCheck注解POST请求的URL
*/
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
} }

View File

@ -5,9 +5,10 @@ import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.shiro.SecurityUtils; import org.jeecg.common.util.LoginUserUtils;
import org.apache.shiro.authz.AuthorizationException; import cn.dev33.satoken.exception.NotLoginException;
import org.apache.shiro.authz.UnauthorizedException; import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import org.jeecg.common.api.dto.LogDTO; import org.jeecg.common.api.dto.LogDTO;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
@ -112,12 +113,34 @@ public class JeecgBootExceptionHandler {
return Result.error("数据库中已存在该记录"); return Result.error("数据库中已存在该记录");
} }
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class}) /**
public Result<?> handleAuthorizationException(AuthorizationException e){ * 处理Sa-Token未登录异常
*/
@ExceptionHandler(NotLoginException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Result<?> handleNotLoginException(NotLoginException e){
log.error("Sa-Token未登录异常: {}", e.getMessage());
return new Result(401, CommonConstant.TOKEN_IS_INVALID_MSG);
}
/**
* 处理Sa-Token无权限异常
*/
@ExceptionHandler(NotPermissionException.class)
public Result<?> handleNotPermissionException(NotPermissionException e){
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.noauth("没有权限,请联系管理员分配权限!"); return Result.noauth("没有权限,请联系管理员分配权限!");
} }
/**
* 处理Sa-Token无角色异常
*/
@ExceptionHandler(NotRoleException.class)
public Result<?> handleNotRoleException(NotRoleException e){
log.error(e.getMessage(), e);
return Result.noauth("没有角色权限,请联系管理员分配角色!");
}
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e){ public Result<?> handleException(Exception e){
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
@ -263,7 +286,7 @@ public class JeecgBootExceptionHandler {
//获取登录用户信息 //获取登录用户信息
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); LoginUser sysUser = LoginUserUtils.getSessionUser();
if(sysUser!=null){ if(sysUser!=null){
log.setUserid(sysUser.getUsername()); log.setUserid(sysUser.getUsername());
log.setUsername(sysUser.getRealname()); log.setUsername(sysUser.getRealname());

View File

@ -6,10 +6,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.beanutils.PropertyUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.LoginUserUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig; import org.jeecg.config.JeecgBaseConfig;
import org.jeecgframework.poi.excel.ExcelImportUtil; import org.jeecgframework.poi.excel.ExcelImportUtil;
@ -52,7 +52,7 @@ public class JeecgController<T, S extends IService<T>> {
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) { protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
// Step.1 组装查询条件 // Step.1 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap()); QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); LoginUser sysUser = LoginUserUtils.getSessionUser();
// 过滤选中数据 // 过滤选中数据
String selections = request.getParameter("selections"); String selections = request.getParameter("selections");
@ -94,7 +94,7 @@ public class JeecgController<T, S extends IService<T>> {
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) { protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
// Step.1 组装查询条件 // Step.1 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap()); QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); LoginUser sysUser = LoginUserUtils.getSessionUser();
// Step.2 计算分页sheet数据 // Step.2 计算分页sheet数据
double total = service.count(); double total = service.count();
int count = (int)Math.ceil(total/pageNum); int count = (int)Math.ceil(total/pageNum);
@ -144,7 +144,7 @@ public class JeecgController<T, S extends IService<T>> {
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) { protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
// 组装查询条件 // 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap()); QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); LoginUser sysUser = LoginUserUtils.getSessionUser();
// 计算分页数 // 计算分页数
double total = service.count(); double total = service.count();
int count = (int) Math.ceil(total / pageSize); int count = (int) Math.ceil(total / pageSize);

View File

@ -1,31 +1,23 @@
package org.jeecg.common.system.util; package org.jeecg.common.system.util;
import com.auth0.jwt.JWT; import cn.dev33.satoken.stp.StpUtil;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
import org.jeecg.common.constant.SymbolConstant; import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.constant.TenantConstant; import org.jeecg.common.constant.TenantConstant;
import org.jeecg.common.util.LoginUserUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.DataBaseConstant;
import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.SysUserCacheInfo; import org.jeecg.common.system.vo.SysUserCacheInfo;
@ -36,159 +28,74 @@ import org.jeecg.common.util.oConvertUtils;
/** /**
* @Author Scott * @Author Scott
* @Date 2018-07-12 14:23 * @Date 2018-07-12 14:23
* @Desc JWT工具类 * @Desc JWT工具类 - 已迁移到Sa-Token此类作为兼容层保留
**/ **/
@Slf4j @Slf4j
public class JwtUtil { public class JwtUtil {
/**PC端Token有效期为7天Token在reids中缓存时间为两倍*/
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000L;
/**APP端Token有效期为30天Token在reids中缓存时间为两倍*/
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET; static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
/** /**
* * 返回错误 JSON 字符串(用于 Sa-Token Filter
* @param response * @param code 错误码
* @param code * @param errorMsg 错误信息
* @param errorMsg * @return JSON 字符串
*/ */
public static void responseError(HttpServletResponse response, Integer code, String errorMsg) { public static String responseErrorJson(Integer code, String errorMsg) {
try { try {
Result jsonResult = new Result(code, errorMsg); Result jsonResult = new Result(code, errorMsg);
jsonResult.setSuccess(false); jsonResult.setSuccess(false);
// 设置响应头和内容类型
response.setStatus(code);
response.setHeader("Content-type", "text/html;charset=UTF-8");
response.setContentType("application/json;charset=UTF-8");
// 使用 ObjectMapper 序列化为 JSON 字符串
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(jsonResult); return objectMapper.writeValueAsString(jsonResult);
response.getWriter().write(json);
response.getWriter().flush();
} catch (IOException e) { } catch (IOException e) {
log.error(e.getMessage(), e); log.error("生成错误 JSON 失败: {}", e.getMessage());
// 返回备用的硬编码 JSON
return "{\"success\":false,\"message\":\"" + errorMsg + "\",\"code\":" + code + ",\"result\":null,\"timestamp\":" + System.currentTimeMillis() + "}";
} }
} }
/** /**
* 校验token是否正确 * 校验token是否正确
* 注意此方法已废弃使用Sa-Token自动校验
* *
* @param token 密钥 * @param token
* @param secret 用户的密码 * @return
* @return 是否正确
*/ */
public static boolean verify(String token, String username, String secret) { @Deprecated
public static boolean verify(String token){
try { try {
// 根据密码生成JWT效验器 // 使用Sa-Token验证
Algorithm algorithm = Algorithm.HMAC256(secret); return StpUtil.getLoginIdByToken(token) != null;
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
// 效验TOKEN
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) { } catch (Exception e) {
log.warn("Token验证失败" + e.getMessage(),e); log.warn(e.getMessage(), e);
return false; return false;
} }
} }
/** /**
* 获得token中的信息无需secret解密也能获得 * 获得Token中的用户名不校验token是否有效
* <p>注意:现在 loginId 就是 username直接返回
* *
* @return token中包含的用户名 * @param token JWT token
* @return 用户名username如果 token 无效则返回 null
*/ */
public static String getUsername(String token) { public static String getUsername(String token){
try { try {
DecodedJWT jwt = JWT.decode(token); if(oConvertUtils.isEmpty(token)) {
return jwt.getClaim("username").asString(); return null;
} catch (JWTDecodeException e) { }
log.error(e.getMessage(), e); // Sa-Token 的 loginId 现在就是 username直接返回
Object loginId = StpUtil.getLoginIdByToken(token);
return loginId != null ? loginId.toString() : null;
} catch (Exception e) {
log.warn("获取用户名失败: {}", e.getMessage());
return null; return null;
} }
} }
/**
* 生成签名,5min后过期
*
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
*/
@Deprecated
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}
/**
* 生成签名,5min后过期
*
* @param username 用户名
* @param secret 用户的密码
* @param expireTime 过期时间
* @return 加密的token
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
*/
@Deprecated
public static String sign(String username, String secret, Long expireTime) {
Date date = new Date(System.currentTimeMillis() + expireTime);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}
/**
* 生成签名,根据客户端类型自动选择过期时间
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
*
* @param username 用户名
* @param secret 用户的密码
* @param clientType 客户端类型PC或APP
* @return 加密的token
*/
public static String sign(String username, String secret, String clientType) {
// 根据客户端类型选择对应的过期时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? APP_EXPIRE_TIME
: EXPIRE_TIME;
Date date = new Date(System.currentTimeMillis() + expireTime);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username和clientType信息
return JWT.create()
.withClaim("username", username)
.withClaim("clientType", clientType)
.withExpiresAt(date)
.sign(algorithm);
}
/**
* 从token中获取客户端类型
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
*
* @param token JWT token
* @return 客户端类型如果不存在则返回PC兼容旧token
*/
public static String getClientType(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
String clientType = jwt.getClaim("clientType").asString();
// 如果clientType为空返回默认值PC兼容旧token
return oConvertUtils.isNotEmpty(clientType) ? clientType : CommonConstant.CLIENT_TYPE_PC;
} catch (JWTDecodeException e) {
log.warn("解析token中的clientType失败使用默认值PC" + e.getMessage());
return CommonConstant.CLIENT_TYPE_PC;
}
}
/** /**
* 根据request中的token获取用户账号 * 根据request中的token获取用户账号
* 注意此方法已适配Sa-Token
* *
* @param request * @param request
* @return * @return
@ -204,7 +111,7 @@ public class JwtUtil {
} }
/** /**
* 从session中获取变量 * 从session中获取变量
* @param key * @param key
* @return * @return
*/ */
@ -215,7 +122,7 @@ public class JwtUtil {
String wellNumber = WELL_NUMBER; String wellNumber = WELL_NUMBER;
if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){ if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){
moshi = key.substring(key.indexOf("}")+1); moshi = key.substring(key.indexOf("}")+1);
} }
String returnValue = null; String returnValue = null;
if (key.contains(wellNumber)) { if (key.contains(wellNumber)) {
@ -231,14 +138,14 @@ public class JwtUtil {
} }
/** /**
* 从当前用户中获取变量 * 从当前用户中获取变量
* @param key * @param key
* @param user * @param user
* @return * @return
*/ */
public static String getUserSystemData(String key, SysUserCacheInfo user) { public static String getUserSystemData(String key, SysUserCacheInfo user) {
//1.优先获取 SysUserCacheInfo //1.优先获取 SysUserCacheInfo
if(user==null) { if (user == null) {
try { try {
user = JeecgDataAutorUtils.loadUserInfo(); user = JeecgDataAutorUtils.loadUserInfo();
} catch (Exception e) { } catch (Exception e) {
@ -248,82 +155,82 @@ public class JwtUtil {
//2.通过shiro获取登录用户信息 //2.通过shiro获取登录用户信息
LoginUser sysUser = null; LoginUser sysUser = null;
try { try {
sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); sysUser = (LoginUser) LoginUserUtils.getSessionUser();
} catch (Exception e) { } catch (Exception e) {
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage()); log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
} }
//#{sys_user_code}% //#{sys_user_code}%
String moshi = ""; String moshi = "";
String wellNumber = WELL_NUMBER; String wellNumber = WELL_NUMBER;
if(key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET)!=-1){ if (key.indexOf(SymbolConstant.RIGHT_CURLY_BRACKET) != -1) {
moshi = key.substring(key.indexOf("}")+1); moshi = key.substring(key.indexOf("}") + 1);
} }
String returnValue = null; String returnValue = null;
//针对特殊标示处理#{sysOrgCode},判断替换 //针对特殊标示处理#{sysOrgCode},判断替换
if (key.contains(wellNumber)) { if (key.contains(wellNumber)) {
key = key.substring(2,key.indexOf("}")); key = key.substring(2, key.indexOf("}"));
} else { } else {
key = key; key = key;
} }
// 是否存在字符串标志 // 是否存在字符串标志
boolean multiStr; boolean multiStr;
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){ if (oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")) {
key = key.substring(1,key.length()-1); key = key.substring(1, key.length() - 1);
multiStr = true; multiStr = true;
} else { } else {
multiStr = false; multiStr = false;
} }
//替换为当前系统时间(年月日) //替换为当前系统时间(年月日)
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) { if (key.equals(DataBaseConstant.SYS_DATE) || key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
returnValue = DateUtils.formatDate(); returnValue = DateUtils.formatDate();
} }
//替换为当前系统时间(年月日时分秒) //替换为当前系统时间(年月日时分秒)
else if (key.equals(DataBaseConstant.SYS_TIME)|| key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) { else if (key.equals(DataBaseConstant.SYS_TIME) || key.toLowerCase().equals(DataBaseConstant.SYS_TIME_TABLE)) {
returnValue = DateUtils.now(); returnValue = DateUtils.now();
} }
//流程状态默认值(默认未发起) //流程状态默认值(默认未发起)
else if (key.equals(DataBaseConstant.BPM_STATUS)|| key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) { else if (key.equals(DataBaseConstant.BPM_STATUS) || key.toLowerCase().equals(DataBaseConstant.BPM_STATUS_TABLE)) {
returnValue = "1"; returnValue = "1";
} }
//后台任务获取用户信息异常,导致程序中断 //后台任务获取用户信息异常,导致程序中断
if(sysUser==null && user==null){ if (sysUser == null && user == null) {
return null; return null;
} }
//替换为系统登录用户帐号 //替换为系统登录用户帐号
if (key.equals(DataBaseConstant.SYS_USER_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) { if (key.equals(DataBaseConstant.SYS_USER_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_USER_CODE_TABLE)) {
if(user==null) { if (user == null) {
returnValue = sysUser.getUsername(); returnValue = sysUser.getUsername();
}else { } else {
returnValue = user.getSysUserCode(); returnValue = user.getSysUserCode();
} }
} }
// 替换为系统登录用户ID // 替换为系统登录用户ID
else if (key.equals(DataBaseConstant.SYS_USER_ID) || key.equalsIgnoreCase(DataBaseConstant.SYS_USER_ID_TABLE)) { else if (key.equals(DataBaseConstant.SYS_USER_ID) || key.equalsIgnoreCase(DataBaseConstant.SYS_USER_ID_TABLE)) {
if(user==null) { if (user == null) {
returnValue = sysUser.getId(); returnValue = sysUser.getId();
}else { } else {
returnValue = user.getSysUserId(); returnValue = user.getSysUserId();
} }
} }
//替换为系统登录用户真实名字 //替换为系统登录用户真实名字
else if (key.equals(DataBaseConstant.SYS_USER_NAME)|| key.toLowerCase().equals(DataBaseConstant.SYS_USER_NAME_TABLE)) { else if (key.equals(DataBaseConstant.SYS_USER_NAME) || key.toLowerCase().equals(DataBaseConstant.SYS_USER_NAME_TABLE)) {
if(user==null) { if (user == null) {
returnValue = sysUser.getRealname(); returnValue = sysUser.getRealname();
}else { } else {
returnValue = user.getSysUserName(); returnValue = user.getSysUserName();
} }
} }
//替换为系统用户登录所使用的机构编码 //替换为系统用户登录所使用的机构编码
else if (key.equals(DataBaseConstant.SYS_ORG_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_ORG_CODE_TABLE)) { else if (key.equals(DataBaseConstant.SYS_ORG_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_ORG_CODE_TABLE)) {
if(user==null) { if (user == null) {
returnValue = sysUser.getOrgCode(); returnValue = sysUser.getOrgCode();
}else { } else {
returnValue = user.getSysOrgCode(); returnValue = user.getSysOrgCode();
} }
} }
@ -338,19 +245,15 @@ public class JwtUtil {
} }
//替换为系统用户所拥有的所有机构编码 //替换为系统用户所拥有的所有机构编码
else if (key.equals(DataBaseConstant.SYS_MULTI_ORG_CODE)|| key.toLowerCase().equals(DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE)) { else if (key.equals(DataBaseConstant.SYS_MULTI_ORG_CODE) || key.toLowerCase().equals(DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE)) {
if(user==null){ if (user == null) {
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
returnValue = sysUser.getOrgCode(); returnValue = sysUser.getOrgCode();
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue; returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
}else{ } else {
if(user.isOneDepart()) { if (user.isOneDepart()) {
returnValue = user.getSysMultiOrgCode().get(0); returnValue = user.getSysMultiOrgCode().get(0);
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue; returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
}else { } else {
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = user.getSysMultiOrgCode().stream() returnValue = user.getSysMultiOrgCode().stream()
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(orgCode -> { .map(orgCode -> {
@ -374,20 +277,17 @@ public class JwtUtil {
} }
} }
// 代码逻辑说明: 多租户ID作为系统变量 // 多租户ID作为系统变量
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)){ else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)) {
try { try {
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID); returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
} catch (Exception e) { } catch (Exception e) {
log.warn("获取系统租户异常:" + e.getMessage()); log.warn("获取系统租户异常:" + e.getMessage());
} }
} }
if(returnValue!=null){returnValue = returnValue + moshi;} if (returnValue != null) {
returnValue = returnValue + moshi;
}
return returnValue; return returnValue;
} }
// public static void main(String[] args) {
// String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjUzMzY1MTMsInVzZXJuYW1lIjoiYWRtaW4ifQ.xjhud_tWCNYBOg_aRlMgOdlZoWFFKB_givNElHNw3X0";
// System.out.println(JwtUtil.getUsername(token));
// }
} }

View File

@ -3,7 +3,9 @@ package org.jeecg.common.system.util;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.annotation.EnumDict; import org.jeecg.common.system.annotation.EnumDict;
import org.jeecg.common.system.vo.DictModel; import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver;
@ -11,7 +13,6 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.*; import java.util.*;
@ -182,10 +183,10 @@ public class ResourceUtil {
for (DictModel dm : dictItemList) { for (DictModel dm : dictItemList) {
String value = dm.getValue(); String value = dm.getValue();
if (keySet.contains(value)) { if (keySet.contains(value)) {
// 修复bug获取或创建该dictCode对应的list而不是每次都创建新的list List<DictModel> list = new ArrayList<>();
List<DictModel> list = map.computeIfAbsent(code, k -> new ArrayList<>());
list.add(new DictModel(value, dm.getText())); list.add(new DictModel(value, dm.getText()));
//break; map.put(code, list);
break;
} }
} }
} }

View File

@ -56,9 +56,7 @@ public class CommonUtils {
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){ public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
String dbPath = null; String dbPath = null;
String fileName = "image" + Math.round(Math.random() * 100000000000L); String fileName = "image" + Math.round(Math.random() * 100000000000L);
//update-begin---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的导致不展示--- fileName += "." + PoiPublicUtil.getFileExtendName(data);
fileName += "." + PoiPublicUtil.getFileExtendName(data).toLowerCase();
//update-end---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的导致不展示---
try { try {
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){ if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
File file = new File(basePath + File.separator + bizPath + File.separator ); File file = new File(basePath + File.separator + bizPath + File.separator );

View File

@ -0,0 +1,175 @@
package org.jeecg.common.util;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.vo.LoginUser;
/**
* 登录用户工具类
* 替代原有的Shiro SecurityUtils工具类
* @author jeecg-boot
*/
@Slf4j
public class LoginUserUtils {
/**
* Session中存储登录用户信息的key
*/
private static final String SESSION_KEY_LOGIN_USER = "loginUser";
/**
* 执行登录并设置用户信息到Session推荐
*
* <p>此方法会:
* <ul>
* <li>1. 调用 StpUtil.login(username) 生成token和session</li>
* <li>2. 将 LoginUser 存入 Session 缓存清除不必要的字段密码等15个字段</li>
* <li>3. 返回生成的 token</li>
* </ul>
*
* @param sysUser 完整的用户对象(从数据库查询得到)
* @return 生成的 token
*/
public static String doLogin(LoginUser sysUser) {
if (sysUser == null) {
throw new IllegalArgumentException("用户对象不能为空");
}
try {
// 1. 获取 username
String username = sysUser.getUsername();
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
// 2. Sa-Token 登录(使用 username 作为 loginId
StpUtil.login(username);
// 3. 用户信息到 LoginUser 并存入 Session
setSessionUser(sysUser);
// 4. 返回生成的 token
return StpUtil.getTokenValue();
} catch (Exception e) {
throw new RuntimeException("登录失败: " + e.getMessage(), e);
}
}
/**
* 获取当前登录用户信息
*
* <p>说明:
* <ul>
* <li>对于需要认证的接口Sa-Token Filter 已经校验过登录状态,此方法必然能获取到用户</li>
* <li>对于已排除拦截的接口:如果未登录或获取失败则返回 null由业务代码自行判断处理</li>
* </ul>
*
* @return 登录用户对象如果未登录或session中没有则返回null
*/
public static LoginUser getSessionUser() {
// 尝试从Sa-Token的Session中获取用户信息
Object loginUser = StpUtil.getSession().get(SESSION_KEY_LOGIN_USER);
if (loginUser instanceof LoginUser) {
return (LoginUser) loginUser;
}
return null;
}
/**
* 根据指定的 token 获取登录用户信息
*
* <p>适用场景:已排除拦截的接口(如 WebSocket需要显式传入 token 来获取用户信息
*
* <p>实现方式:临时切换到该 token 对应的会话,然后获取用户信息
*
* @param token JWT token
* @return 登录用户对象,如果 token 无效或session中没有则返回null
*/
public static LoginUser getSessionUser(String token) {
try {
// 根据 token 获取登录ID
Object loginId = StpUtil.getLoginIdByToken(token);
if (loginId == null) {
return null;
}
// 临时切换到该 token 对应的登录会话
StpUtil.switchTo(loginId);
// 直接调用无参方法获取用户信息
return getSessionUser();
} catch (Exception e) {
log.debug("根据token获取用户信息失败: {}", e.getMessage());
return null;
}
}
/**
* 设置当前登录用户信息到Session
*
* <p>为减少 Redis 存储和保障安全,只保留必要的核心字段:
* <ul>
* <li>id, username, realname - 基础用户信息</li>
* <li>orgCode, orgId, departIds - 部门和数据权限</li>
* <li>roleCode - 角色权限</li>
* <li>loginTenantId, relTenantIds - 多租户</li>
* <li>avatar - 用户头像</li>
* </ul>
*
* <p>⚠️ 注意:调用此方法前需要先调用 StpUtil.login()
*
* @param loginUser 登录用户对象
*/
public static void setSessionUser(LoginUser loginUser) {
if (loginUser == null) {
return;
}
// ⚠️ 安全与性能:清除不必要的字段,减少 Redis 存储
loginUser.setPassword(null); // 密码(安全)
loginUser.setWorkNo(null); // 工号
loginUser.setBirthday(null); // 生日
loginUser.setSex(null); // 性别
loginUser.setEmail(null); // 邮箱
loginUser.setPhone(null); // 手机号
loginUser.setStatus(null); // 状态
loginUser.setDelFlag(null); // 删除标志
loginUser.setActivitiSync(null); // 工作流同步
loginUser.setCreateTime(null); // 创建时间
loginUser.setUserIdentity(null); // 用户身份
loginUser.setPost(null); // 职务
loginUser.setTelephone(null); // 座机
loginUser.setRelTenantIds(null); // 关联租户
loginUser.setMainDepPostId(null); // 主岗位
StpUtil.getSession().set(SESSION_KEY_LOGIN_USER, loginUser);
}
/**
* 获取当前登录用户名(推荐使用此方法,语义更清晰)
* @return 用户名username
*/
public static String getUsername() {
return StpUtil.getLoginIdAsString();
}
/**
* 检查是否已登录
* @return true-已登录false-未登录
*/
public static boolean isLogin() {
return StpUtil.isLogin();
}
/**
* 退出登录
*/
public static void logout() {
StpUtil.logout();
}
}

View File

@ -46,7 +46,7 @@ public class RestUtil {
public static String getBaseUrl() { public static String getBaseUrl() {
String basepath = getDomain() + getPath(); String basepath = getDomain() + getPath();
log.debug(" RestUtil.getBaseUrl: " + basepath); log.info(" RestUtil.getBaseUrl: " + basepath);
return basepath; return basepath;
} }
@ -199,7 +199,7 @@ public class RestUtil {
* @return ResponseEntity<responseType> * @return ResponseEntity<responseType>
*/ */
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) { public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
log.debug(" RestUtil --- request --- url = "+ url); log.info(" RestUtil --- request --- url = "+ url);
if (StringUtils.isEmpty(url)) { if (StringUtils.isEmpty(url)) {
throw new RuntimeException("url 不能为空"); throw new RuntimeException("url 不能为空");
} }
@ -230,7 +230,7 @@ public class RestUtil {
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH); String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(contentLength))) { if (current == null || !current.equals(String.valueOf(contentLength))) {
headers.setContentLength(contentLength); headers.setContentLength(contentLength);
log.debug(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":"")); log.info(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
} }
} }
// 发送请求 // 发送请求
@ -252,7 +252,7 @@ public class RestUtil {
*/ */
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
JSONObject variables, Object params, Class<T> responseType, int timeout) { JSONObject variables, Object params, Class<T> responseType, int timeout) {
log.debug(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout); log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
if (StringUtils.isEmpty(url)) { if (StringUtils.isEmpty(url)) {
throw new RuntimeException("url 不能为空"); throw new RuntimeException("url 不能为空");
@ -302,7 +302,7 @@ public class RestUtil {
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH); String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(contentLength))) { if (current == null || !current.equals(String.valueOf(contentLength))) {
headers.setContentLength(contentLength); headers.setContentLength(contentLength);
log.debug(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":"")); log.info(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
} }
} }

View File

@ -1,37 +0,0 @@
package org.jeecg.common.util;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import java.util.concurrent.*;
/**
* @date 2025-09-04
* @author scott
*
* @Description: 支持shiro的API获取当前登录人方法的线程池
*/
public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
public ShiroThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
Subject subject = SecurityUtils.getSubject();
SecurityManager securityManager = SecurityUtils.getSecurityManager();
super.execute(() -> {
try {
ThreadContext.bind(securityManager);
ThreadContext.bind(subject);
command.run();
} finally {
ThreadContext.unbindSubject();
ThreadContext.unbindSecurityManager();
}
});
}
}

View File

@ -1,5 +1,6 @@
package org.jeecg.common.util; package org.jeecg.common.util;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.api.CommonAPI; import org.jeecg.common.api.CommonAPI;
@ -87,73 +88,40 @@ public class TokenUtils {
} }
/** /**
* 验证Token * 验证Token已重写为Sa-Token实现
*/ */
public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi, RedisUtil redisUtil) { public static boolean verifyToken(HttpServletRequest request, CommonAPI commonApi) {
log.debug(" -- url --" + request.getRequestURL()); log.debug(" -- url --" + request.getRequestURL());
String token = getTokenByRequest(request); String token = getTokenByRequest(request);
return TokenUtils.verifyToken(token, commonApi, redisUtil); return TokenUtils.verifyToken(token, commonApi);
} }
/** /**
* 验证Token * 验证Token已重写为Sa-Token实现
*/ */
public static boolean verifyToken(String token, CommonAPI commonApi, RedisUtil redisUtil) { public static boolean verifyToken(String token, CommonAPI commonApi) {
if (StringUtils.isBlank(token)) { if (StringUtils.isBlank(token)) {
throw new JeecgBoot401Exception("token不能为空!"); throw new JeecgBoot401Exception("token不能为空!");
} }
// 解密获得username用于和数据库进行对比 // 使用Sa-Token校验token
String username = JwtUtil.getUsername(token); Object username = StpUtil.getLoginIdByToken(token);
if (username == null) { if (username == null) {
throw new JeecgBoot401Exception("token非法无效!"); throw new JeecgBoot401Exception("token非法无效!");
} }
// 查询用户信息 // 查询用户信息
LoginUser user = TokenUtils.getLoginUser(username, commonApi, redisUtil); LoginUser user = commonApi.getUserByName(username.toString());
//LoginUser user = commonApi.getUserByName(username);
if (user == null) { if (user == null) {
throw new JeecgBoot401Exception("用户不存在!"); throw new JeecgBoot401Exception("用户不存在!");
} }
// 判断用户状态 // 判断用户状态
if (user.getStatus() != 1) { if (user.getStatus() != 1) {
throw new JeecgBoot401Exception("账号已被锁定,请联系管理员!"); throw new JeecgBoot401Exception("账号已被锁定,请联系管理员!");
} }
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
// 用户登录Token过期提示信息
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
throw new JeecgBoot401Exception(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
}
return true;
}
/** return true;
* 刷新token保证用户在线操作不掉线
* @param token
* @param userName
* @param passWord
* @param redisUtil
* @return
*/
private static boolean jwtTokenRefresh(String token, String userName, String passWord, RedisUtil redisUtil) {
String cacheToken = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
// 从token中解析客户端类型保持续期时使用相同的客户端类型
String clientType = JwtUtil.getClientType(token);
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
// 根据客户端类型设置对应的缓存有效时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
: JwtUtil.EXPIRE_TIME * 2 / 1000;
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
}
return true;
}
return false;
} }
/** /**
@ -181,4 +149,5 @@ public class TokenUtils {
} }
return loginUser; return loginUser;
} }
} }

View File

@ -1,12 +1,13 @@
package org.jeecg.common.util.encryption; package org.jeecg.common.util.encryption;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.lang.codec.Base64;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Base64;
/** /**
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding) * AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
@ -23,7 +24,7 @@ public class AesEncryptUtil {
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES"); SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8)); IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec); cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
byte[] plain = cipher.doFinal(Base64.decode(cipherBase64)); byte[] plain = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
return new String(plain, StandardCharsets.UTF_8); return new String(plain, StandardCharsets.UTF_8);
} }
@ -33,7 +34,7 @@ public class AesEncryptUtil {
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES"); SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8)); IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec); cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
byte[] data = cipher.doFinal(Base64.decode(cipherBase64)); byte[] data = cipher.doFinal(Base64.getDecoder().decode(cipherBase64));
return new String(data, StandardCharsets.UTF_8) return new String(data, StandardCharsets.UTF_8)
.replace("\u0000",""); // 旧填充 0 .replace("\u0000",""); // 旧填充 0
} }
@ -93,7 +94,7 @@ public class AesEncryptUtil {
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8)); IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec); cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(plaintext); byte[] encrypted = cipher.doFinal(plaintext);
return Base64.encodeToString(encrypted); return Base64.getEncoder().encodeToString(encrypted);
}catch(Exception e){ }catch(Exception e){
throw new IllegalStateException("legacy encrypt error", e); throw new IllegalStateException("legacy encrypt error", e);
} }

View File

@ -1232,25 +1232,4 @@ public class oConvertUtils {
.toArray(String[]::new); .toArray(String[]::new);
} }
/**
* String转换long类型
*
* @param v
* @param def
* @return
*/
public static long getLong(Object v, long def) {
if (v == null) {
return def;
};
if (v instanceof Number) {
return ((Number) v).longValue();
}
try {
return Long.parseLong(v.toString());
} catch (Exception e) {
return def;
}
}
} }

View File

@ -1,24 +0,0 @@
package org.jeecg.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* ai配置类通用的配置可以放到这里面
*
* @Author: wangshuai
* @Date: 2025/12/17 14:00
*/
@Data
@Component
@ConfigurationProperties(prefix = AiRagConfigBean.PREFIX)
public class AiRagConfigBean {
public static final String PREFIX = "jeecg.airag";
/**
* 敏感节点
* stdio mpc命令行功能开启sqlAI流程SQL节点开启
*/
private String allowSensitiveNodes = "";
}

View File

@ -12,6 +12,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* @author eightmonth@qq.com
* 启动程序修改DruidWallConfig配置 * 启动程序修改DruidWallConfig配置
* 允许SELECT语句的WHERE子句是一个永真条件 * 允许SELECT语句的WHERE子句是一个永真条件
* @author eightmonth * @author eightmonth

View File

@ -136,7 +136,7 @@ public class Swagger3Config implements WebMvcConfigurer {
return new OpenAPI() return new OpenAPI()
.info(new Info() .info(new Info()
.title("JeecgBoot 后台服务API接口文档") .title("JeecgBoot 后台服务API接口文档")
.version("3.9.1") .version("3.9.0")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com")) .contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description("后台API接口") .description("后台API接口")
.termsOfService("NO terms of service") .termsOfService("NO terms of service")

View File

@ -1,32 +0,0 @@
package org.jeecg.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* 任务调度器配置
* 提供 ThreadPoolTaskScheduler Bean 用于 AI RAG 流程调度等功能
* 仅当容器中不存在 ThreadPoolTaskScheduler 时才创建
*
* @author jeecg
*/
@Slf4j
@Configuration
public class TaskSchedulerConfig {
@Bean
@ConditionalOnMissingBean(ThreadPoolTaskScheduler.class)
public ThreadPoolTaskScheduler taskScheduler() {
log.info("初始化定时任务调度器 ThreadPoolTaskScheduler");
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("airag-scheduler-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
return scheduler;
}
}

View File

@ -24,23 +24,18 @@ public class WebsocketFilter implements Filter {
private static CommonAPI commonApi; private static CommonAPI commonApi;
private static RedisUtil redisUtil;
@Override @Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (commonApi == null) { if (commonApi == null) {
commonApi = SpringContextUtils.getBean(CommonAPI.class); commonApi = SpringContextUtils.getBean(CommonAPI.class);
} }
if (redisUtil == null) {
redisUtil = SpringContextUtils.getBean(RedisUtil.class);
}
HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletRequest request = (HttpServletRequest)servletRequest;
String token = request.getHeader(TOKEN_KEY); String token = request.getHeader(TOKEN_KEY);
log.debug("Websocket连接 Token安全校验Path = {}token:{}", request.getRequestURI(), token); log.debug("Websocket连接 Token安全校验Path = {}token:{}", request.getRequestURI(), token);
try { try {
TokenUtils.verifyToken(token, commonApi, redisUtil); TokenUtils.verifyToken(token, commonApi);
} catch (Exception exception) { } catch (Exception exception) {
//log.error("Websocket连接 Token安全校验失败IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage()); //log.error("Websocket连接 Token安全校验失败IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());
log.debug("Websocket连接 Token安全校验失败IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage()); log.debug("Websocket连接 Token安全校验失败IP:{}, Token:{}, Path = {},异常:{}", oConvertUtils.getIpAddrByRequest(request), token, request.getRequestURI(), exception.getMessage());

View File

@ -2,7 +2,7 @@ package org.jeecg.config.firewall.interceptor;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils; import org.jeecg.common.util.LoginUserUtils;
import org.jeecg.common.api.CommonAPI; import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
@ -68,7 +68,7 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) { if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
String requestURI = request.getRequestURI().substring(request.getContextPath().length()); String requestURI = request.getRequestURI().substring(request.getContextPath().length());
log.info("低代码模式,拦截请求路径:" + requestURI); log.info("低代码模式,拦截请求路径:" + requestURI);
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); LoginUser loginUser = LoginUserUtils.getSessionUser();
Set<String> hasRoles = null; Set<String> hasRoles = null;
if (loginUser == null) { if (loginUser == null) {
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest())); loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));

View File

@ -6,7 +6,7 @@ import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*; import org.apache.ibatis.plugin.*;
import org.apache.shiro.SecurityUtils; import org.jeecg.common.util.LoginUserUtils;
import org.jeecg.common.config.TenantContext; import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.TenantConstant; import org.jeecg.common.constant.TenantConstant;
import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.system.vo.LoginUser;
@ -189,7 +189,7 @@ public class MybatisInterceptor implements Interceptor {
private LoginUser getLoginUser() { private LoginUser getLoginUser() {
LoginUser sysUser = null; LoginUser sysUser = null;
try { try {
sysUser = SecurityUtils.getSubject().getPrincipal() != null ? (LoginUser) SecurityUtils.getSubject().getPrincipal() : null; sysUser = LoginUserUtils.getSessionUser() != null ? LoginUserUtils.getSessionUser() : null;
} catch (Exception e) { } catch (Exception e) {
//e.printStackTrace(); //e.printStackTrace();
sysUser = null; sysUser = null;

View File

@ -1,4 +1,4 @@
package org.jeecg.config.shiro; package org.jeecg.config.satoken;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -16,3 +16,4 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreAuth { public @interface IgnoreAuth {
} }

View File

@ -0,0 +1,420 @@
package org.jeecg.config.satoken;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.DispatcherType;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.*;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.config.satoken.ignore.InMemoryIgnoreAuth;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Role;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author: jeecg-boot
* @description: Sa-Token 配置类
*/
@Slf4j
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SaTokenConfig implements WebMvcConfigurer {
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private Environment env;
@Autowired
private CommonAPI commonAPI;
@Autowired
private RedisUtil redisUtil;
/**
* Sa-Token 整合 jwt (Simple 模式)
* 使用JWT-Simple模式生成标准JWT格式的token
* 并支持从URL参数"token"读取token兼容原系统
*/
@Bean
@Primary
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple() {
/**
* 获取当前请求的 Token 值
* 优先级Header > URL参数token > URL参数X-Access-Token
*/
@Override
public String getTokenValue() {
try {
SaRequest request = SaHolder.getRequest();
// 1. 优先从Header中获取
String tokenValue = request.getHeader(getConfigOrGlobal().getTokenName());
if (oConvertUtils.isNotEmpty(tokenValue)) {
return tokenValue;
}
// 2. 从URL参数"token"获取(兼容原系统)
tokenValue = request.getParam("token");
if (oConvertUtils.isNotEmpty(tokenValue)) {
return tokenValue;
}
// 3. 从URL参数"X-Access-Token"获取
tokenValue = request.getParam(getConfigOrGlobal().getTokenName());
if (oConvertUtils.isNotEmpty(tokenValue)) {
return tokenValue;
}
} catch (Exception e) {
log.debug("获取token失败: {}", e.getMessage());
}
// 4. 如果都没有,使用默认逻辑
return super.getTokenValue();
}
};
}
/**
* 注册 Sa-Token 拦截器,打开注解式鉴权功能
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
/**
* 注册 Sa-Token 全局过滤器
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定 [拦截路由] 与 [放行路由]
.addInclude("/**")
.setExcludeList(getExcludeUrls())
// 认证函数: 每次请求执行
.setAuth(obj -> {
// 检查是否是免认证路径
String servletPath = SaHolder.getRequest().getRequestPath();
if (InMemoryIgnoreAuth.contains(servletPath)) {
return;
}
// 校验 token如果请求中带有 token先切换到对应的登录会话再校验
try {
String token = StpUtil.getTokenValue();
if (oConvertUtils.isNotEmpty(token)) {
// 根据 token 获取 loginId 并切换到对应的登录会话
Object loginId = StpUtil.getLoginIdByToken(token);
if (loginId != null) {
StpUtil.switchTo(loginId);
// 需要手工自动续签默认参数auto-renew:true 不好使
long activeTimeout = StpUtil.stpLogic.getConfigOrGlobal().getActiveTimeout();
if (activeTimeout > 0) {
// 获取当前token的活跃剩余时间
long tokenActiveTimeout = StpUtil.getTokenActiveTimeout();
// 如果剩余活跃时间少于总活跃时间的一半,进行续签
if (tokenActiveTimeout > 0 && tokenActiveTimeout < (activeTimeout / 2)) {
StpUtil.stpLogic.updateLastActiveToNow(token);
log.info("【Sa-Token拦截器】Token续签成功剩余活跃时间: {}秒", tokenActiveTimeout);
}
}
}
}
} catch (Exception e) {
// 如果获取 loginId 失败,说明 token 无效或未登录,让 checkLogin 抛出异常
log.debug("切换登录会话失败: {}", e.getMessage());
}
// 最终校验登录状态
StpUtil.checkLogin();
// 租户校验逻辑
checkTenantAuthorization();
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
log.warn("Sa-Token 认证失败用户未登录或token无效");
log.warn("请求路径: {}, Method: {}Token: {}", SaHolder.getRequest().getRequestPath(), SaHolder.getRequest().getMethod(), StpUtil.getTokenValue());
// 返回401状态码
SaHolder.getResponse().setStatus(401).setHeader("Content-Type", "application/json;charset=UTF-8");
return org.jeecg.common.system.util.JwtUtil.responseErrorJson(401, CommonConstant.TOKEN_IS_INVALID_MSG);
})
// 前置函数在每次认证函数之前执行BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
.setBeforeAuth(r -> {
// 设置跨域配置
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
// 如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
if (cloudServer == null) {
SaHolder.getResponse()
// 允许指定域访问跨域资源
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, SaHolder.getRequest().getHeader(HttpHeaders.ORIGIN))
// 允许所有请求方式
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS")
// 有效时间
.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600")
// 允许的header参数
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, SaHolder.getRequest().getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS))
// 允许携带凭证
.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}
// OPTIONS预检请求直接返回
SaRouter.match(SaHttpMethod.OPTIONS).free(r2 -> {
SaHolder.getResponse().setStatus(HttpStatus.OK.value());
});
// 设置当前线程上下文的租户ID
String tenantId = SaHolder.getRequest().getHeader(CommonConstant.TENANT_ID);
TenantContext.setTenant(tenantId);
log.debug("===【TenantContext 线程设置】=== 请求路径: {}, 租户ID: {}", SaHolder.getRequest().getRequestPath(), tenantId);
});
}
/**
* spring过滤装饰器 <br/>
* 支持异步请求的过滤器装饰
*/
@Bean
public FilterRegistrationBean<SaServletFilter> saTokenFilterRegistration() {
FilterRegistrationBean<SaServletFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(getSaServletFilter());
registration.setName("SaServletFilter");
// 支持异步请求
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
// 拦截所有请求
registration.addUrlPatterns("/*");
registration.setOrder(1);
registration.setAsyncSupported(true); // 支持异步请求
return registration;
}
/**
* 获取排除URL列表
*/
private List<String> getExcludeUrls() {
List<String> excludeUrls = new ArrayList<>();
// 支持yml方式配置拦截排除
if (jeecgBaseConfig != null && jeecgBaseConfig.getShiro() != null) {
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
if (oConvertUtils.isNotEmpty(shiroExcludeUrls)) {
String[] permissionUrl = shiroExcludeUrls.split(",");
excludeUrls.addAll(Arrays.asList(permissionUrl));
}
}
// 添加默认排除路径
excludeUrls.addAll(Arrays.asList(
"/sys/cas/client/validateLogin", // cas验证登录
"/sys/randomImage/**", // 登录验证码接口排除
"/sys/checkCaptcha", // 登录验证码接口排除
"/sys/smsCheckCaptcha", // 短信次数发送太多验证码排除
"/sys/login", // 登录接口排除
"/sys/mLogin", // 登录接口排除
"/sys/logout", // 登出接口排除
"/sys/thirdLogin/**", // 第三方登录
"/sys/getEncryptedString", // 获取加密串
"/sys/sms", // 短信验证码
"/sys/phoneLogin", // 手机登录
"/sys/user/checkOnlyUser", // 校验用户是否存在
"/sys/user/register", // 用户注册
"/sys/user/phoneVerification", // 用户忘记密码验证手机号
"/sys/user/passwordChange", // 用户更改密码
"/auth/2step-code", // 登录验证码
"/sys/common/static/**", // 图片预览 & 下载文件不限制token
"/sys/common/pdf/**", // pdf预览
"/generic/**", // pdf预览需要文件
"/sys/getLoginQrcode/**", // 登录二维码
"/sys/getQrcodeToken/**", // 监听扫码
"/sys/checkAuth", // 授权接口排除
"/openapi/call/**", // 开放平台接口排除
// 排除静态资源后缀
"/",
"/doc.html",
"**/*.js",
"**/*.css",
"**/*.html",
"**/*.svg",
"**/*.pdf",
"**/*.jpg",
"**/*.png",
"**/*.gif",
"**/*.ico",
"**/*.ttf",
"**/*.woff",
"**/*.woff2",
"**/*.glb",
"**/*.wasm",
"**/*.js.map",
"**/*.css.map",
"/druid/**",
"/swagger-ui.html",
"/swagger*/**",
"/webjars/**",
"/v3/**",
// 排除消息通告查看详情页面用于第三方APP
"/sys/annountCement/show/**",
// 积木报表和积木BI排除
"/jmreport/**",
"/drag/lib/**",
"/drag/list/**",
"/drag/favicon.ico",
"/drag/view",
"/drag/page/queryById",
"/drag/page/addVisitsNumber",
"/drag/page/queryTemplateList",
"/drag/share/view/**",
"/drag/onlDragDatasetHead/getAllChartData",
"/drag/onlDragDatasetHead/getTotalData",
"/drag/onlDragDatasetHead/getMapDataByCode",
"/drag/onlDragDatasetHead/getTotalDataByCompId",
"/drag/mock/json/**",
"/drag/onlDragDatasetHead/getDictByCodes",
"/drag/onlDragDatasetHead/queryAllById",
"/jimubi/view",
"/jimubi/share/view/**",
// 大屏模板例子
"/test/bigScreen/**",
"/bigscreen/template1/**",
"/bigscreen/template2/**",
// websocket排除
"/websocket/**", // 系统通知和公告
"/newsWebsocket/**", // CMS模块
"/vxeSocket/**", // JVxeTable无痕刷新示例
"/dragChannelSocket/**", // 仪表盘(按钮通信)
// App vue3版本查询版本接口
"/sys/version/app3version",
// 测试模块排除
"/test/seata/**",
// 错误路径排除
"/error",
// 企业微信证书排除
"/WW_verify*"
));
return excludeUrls;
}
/**
* 校验用户的tenant_id和前端传过来的是否一致
*
* <p>实现逻辑:
* <ul>
* <li>1. 获取当前登录用户信息</li>
* <li>2. 检查用户是否配置了租户信息</li>
* <li>3. 获取前端请求头中的租户ID</li>
* <li>4. 校验用户所属租户中是否包含当前请求的租户ID</li>
* <li>5. 如果校验失败,从数据库重新查询用户信息并再次校验</li>
* <li>6. 最终校验失败则抛出异常</li>
* </ul>
*
* @throws NotLoginException 租户授权变更异常
*/
private void checkTenantAuthorization() {
log.debug("------ 租户校验开始 ------");
// 如果未开启租户控制,直接返回
if (!MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
return;
}
try {
// 获取当前登录用户信息
LoginUser loginUser = TokenUtils.getLoginUser(LoginUserUtils.getUsername(), commonAPI, redisUtil);
if (loginUser == null) {
return;
}
String username = loginUser.getUsername();
String userTenantIds = loginUser.getRelTenantIds();
// 如果用户未配置租户信息,直接返回
if (oConvertUtils.isEmpty(userTenantIds)) {
return;
}
// 获取前端请求头中的租户ID
String loginTenantId = TokenUtils.getTenantIdByRequest(SpringContextUtils.getHttpServletRequest());
log.info("登录租户:{}", loginTenantId);
log.info("用户拥有那些租户:{}", userTenantIds);
// 登录用户无租户前端header中租户ID值为 0
String str = "0";
if (oConvertUtils.isEmpty(loginTenantId) || str.equals(loginTenantId)) {
return;
}
String[] userTenantIdsArray = userTenantIds.split(",");
if (!oConvertUtils.isIn(loginTenantId, userTenantIdsArray)) {
boolean isAuthorization = false;
//========================================================================
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
redisUtil.del(loginUserKey);
LoginUser loginUserFromDb = commonAPI.getUserByName(username);
LoginUserUtils.setSessionUser(loginUserFromDb);
if (loginUserFromDb != null && oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
if (oConvertUtils.isIn(loginTenantId, newArray)) {
isAuthorization = true;
}
}
//========================================================================
if (!isAuthorization) {
log.info("租户异常——登录租户:{}", loginTenantId);
log.info("租户异常——用户拥有租户组:{}", userTenantIds);
throw new NotLoginException("登录租户授权变更,请重新登陆!", StpUtil.TYPE, NotLoginException.KICK_OUT);
}
}
}catch (Exception e) {
log.error("租户校验异常:{}", e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,174 @@
package org.jeecg.config.satoken;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.stp.StpInterface;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.CommonAPI;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* @description: Sa-Token 权限认证接口实现(带缓存)
*
* <p>⚠️ 重要说明:</p>
* <ul>
* <li><strong>Sa-Token 的 StpInterface 默认不提供缓存能力</strong>,需要自己实现缓存逻辑</li>
* <li>本实现采用 <strong>[账号id -> 权限/角色列表]</strong> 缓存模型</li>
* <li>缓存键格式:
* <ul>
* <li>用户权限缓存satoken:user-permission:{username}</li>
* <li>用户角色缓存satoken:user-role:{username}</li>
* </ul>
* </li>
* <li>缓存过期时间30天</li>
* <li>⚠️ 当修改用户的角色或权限时,需要手动清除缓存</li>
* </ul>
*
* <p>清除缓存示例:</p>
* <pre>
* // 清除单个用户的权限和角色缓存
* StpInterfaceImpl.clearUserCache("admin");
*
* // 清除多个用户的缓存
* StpInterfaceImpl.clearUserCache(Arrays.asList("admin", "user1", "user2"));
* </pre>
*/
@Component
@Slf4j
public class StpInterfaceImpl implements StpInterface {
@Lazy
@Resource
private CommonAPI commonApi;
/**
* 缓存过期时间30天
*/
private static final long CACHE_TIMEOUT = 60 * 60 * 24 * 30;
/**
* 权限缓存键前缀
*/
private static final String PERMISSION_CACHE_PREFIX = "satoken:user-permission:";
/**
* 角色缓存键前缀
*/
private static final String ROLE_CACHE_PREFIX = "satoken:user-role:";
/**
* 返回一个账号所拥有的权限码集合(带缓存)
*
* @param loginId 账号id这里是 username
* @param loginType 账号类型
* @return 权限码集合
*/
@Override
@SuppressWarnings("unchecked")
public List<String> getPermissionList(Object loginId, String loginType) {
String username = loginId.toString();
String cacheKey = PERMISSION_CACHE_PREFIX + username;
SaTokenDao dao = SaManager.getSaTokenDao();
// 1. 先从缓存获取
List<String> permissionList = (List<String>) dao.getObject(cacheKey);
if (permissionList == null) {
// 2. 缓存不存在,从数据库查询
log.warn("权限缓存未命中,查询数据库 [ username={} ]", username);
String userId = commonApi.getUserIdByName(username);
if (userId == null) {
log.warn("用户不存在: {}", username);
return new ArrayList<>();
}
Set<String> permissionSet = commonApi.queryUserAuths(userId);
permissionList = new ArrayList<>(permissionSet);
// 3. 将结果缓存起来
dao.setObject(cacheKey, permissionList, CACHE_TIMEOUT);
log.info("权限已缓存 [ username={}, permissions={} ]", username, permissionList.size());
} else {
log.debug("权限缓存命中 [ username={}, permissions={} ]", username, permissionList.size());
}
return permissionList;
}
/**
* 返回一个账号所拥有的角色标识集合(带缓存)
*
* @param loginId 账号id这里是 username
* @param loginType 账号类型
* @return 角色标识集合
*/
@Override
@SuppressWarnings("unchecked")
public List<String> getRoleList(Object loginId, String loginType) {
String username = loginId.toString();
String cacheKey = ROLE_CACHE_PREFIX + username;
SaTokenDao dao = SaManager.getSaTokenDao();
// 1. 先从缓存获取
List<String> roleList = (List<String>) dao.getObject(cacheKey);
if (roleList == null) {
// 2. 缓存不存在,从数据库查询
log.warn("角色缓存未命中,查询数据库 [ username={} ]", username);
String userId = commonApi.getUserIdByName(username);
if (userId == null) {
log.warn("用户不存在: {}", username);
return new ArrayList<>();
}
Set<String> roleSet = commonApi.queryUserRolesById(userId);
roleList = new ArrayList<>(roleSet);
// 3. 将结果缓存起来
dao.setObject(cacheKey, roleList, CACHE_TIMEOUT);
log.info("角色已缓存 [ username={}, roles={} ]", username, roleList.size());
} else {
log.debug("角色缓存命中 [ username={}, roles={} ]", username, roleList.size());
}
return roleList;
}
/**
* 清除单个用户的权限和角色缓存
* <p>使用场景:修改用户的角色分配后</p>
*
* @param username 用户名
*/
public static void clearUserCache(String username) {
SaTokenDao dao = SaManager.getSaTokenDao();
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
dao.deleteObject(ROLE_CACHE_PREFIX + username);
log.info("已清除用户缓存 [ username={} ]", username);
}
/**
* 批量清除多个用户的权限和角色缓存
* <p>使用场景:修改角色权限后,清除拥有该角色的所有用户的缓存</p>
*
* @param usernameList 用户名列表
*/
public static void clearUserCache(List<String> usernameList) {
SaTokenDao dao = SaManager.getSaTokenDao();
for (String username : usernameList) {
dao.deleteObject(PERMISSION_CACHE_PREFIX + username);
dao.deleteObject(ROLE_CACHE_PREFIX + username);
}
log.info("已批量清除用户缓存 [ count={} ]", usernameList.size());
}
}

View File

@ -1,14 +1,13 @@
package org.jeecg.config.shiro.ignore; package org.jeecg.config.satoken.ignore;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.config.shiro.IgnoreAuth; import org.jeecg.config.satoken.IgnoreAuth;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@ -38,12 +37,12 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
// 优化直接从HandlerMethod过滤避免重复扫描 // 优化直接从HandlerMethod过滤避免重复扫描
requestMappingHandlerMapping.getHandlerMethods().values().stream() requestMappingHandlerMapping.getHandlerMethods().values().stream()
.filter(handlerMethod -> handlerMethod.getMethod().isAnnotationPresent(IgnoreAuth.class)) .filter(handlerMethod -> handlerMethod.getMethod().isAnnotationPresent(IgnoreAuth.class))
.forEach(handlerMethod -> { .forEach(handlerMethod -> {
Class<?> clazz = handlerMethod.getBeanType(); Class<?> clazz = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod(); Method method = handlerMethod.getMethod();
ignoreAuthUrls.addAll(processIgnoreAuthMethod(clazz, method)); ignoreAuthUrls.addAll(processIgnoreAuthMethod(clazz, method));
}); });
log.info("Init Token ignoreAuthUrls Config [ 集合 ] {}", ignoreAuthUrls); log.info("Init Token ignoreAuthUrls Config [ 集合 ] {}", ignoreAuthUrls);
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) { if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {

View File

@ -1,4 +1,4 @@
package org.jeecg.config.shiro.ignore; package org.jeecg.config.satoken.ignore;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher; import org.springframework.util.PathMatcher;
@ -6,8 +6,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* 使用内存存储通过@IgnoreAuth注解的url配合JwtFilter进行免登录校验 * 使用内存存储通过@IgnoreAuth注解的url配合Sa-Token进行免登录校验
* PS无法使用ThreadLocal进行存储因为ThreadLocal装载时JwtFilter已经初始化完毕导致该类获取ThreadLocal为空 * PS无法使用ThreadLocal进行存储因为ThreadLocal装载时Filter已经初始化完毕导致该类获取ThreadLocal为空
* @author eightmonth * @author eightmonth
* @date 2024/4/18 15:02 * @date 2024/4/18 15:02
*/ */
@ -15,6 +15,7 @@ public class InMemoryIgnoreAuth {
private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>(); private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>();
private static PathMatcher MATCHER = new AntPathMatcher(); private static PathMatcher MATCHER = new AntPathMatcher();
public InMemoryIgnoreAuth() {} public InMemoryIgnoreAuth() {}
public static void set(List<String> list) { public static void set(List<String> list) {
@ -31,11 +32,11 @@ public class InMemoryIgnoreAuth {
public static boolean contains(String url) { public static boolean contains(String url) {
for (String ignoreAuth : IGNORE_AUTH_LIST) { for (String ignoreAuth : IGNORE_AUTH_LIST) {
if(MATCHER.match(ignoreAuth,url)){ if(MATCHER.match(ignoreAuth, url)){
return true; return true;
} }
} }
return false; return false;
} }
} }

View File

@ -1,28 +0,0 @@
package org.jeecg.config.shiro;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @Author Scott
* @create 2018-07-12 15:19
* @desc
**/
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}

View File

@ -1,395 +0,0 @@
package org.jeecg.config.shiro;
import jakarta.annotation.Resource;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.ShiroUrlPathHelper;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.*;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
import org.jeecg.config.shiro.filters.JwtFilter;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import java.lang.reflect.Method;
import java.util.*;
/**
* @author: Scott
* @date: 2018/2/7
* @description: shiro 配置类
*/
@Slf4j
@Configuration
public class ShiroConfig {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
@Autowired
private Environment env;
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired(required = false)
private RedisProperties redisProperties;
/**
* Filter Chain定义说明
*
* 1、一个URL可以配置多个Filter使用逗号分隔
* 2、当设置多个过滤器时全部验证通过才视为通过
* 3、部分过滤器可指定参数如permsroles
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//支持yml方式配置拦截排除
if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){
String[] permissionUrl = shiroExcludeUrls.split(",");
for(String url : permissionUrl){
filterChainDefinitionMap.put(url,"anon");
}
}
}
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/smsCheckCaptcha", "anon"); //短信次数发送太多验证码排除
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串
filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码
filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录
filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在
filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册
filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号
filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码
filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码
filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token
filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览
//filterChainDefinitionMap.put("/sys/common/view/**", "anon");//图片预览不限制token
//filterChainDefinitionMap.put("/sys/common/download/**", "anon");//文件下载不限制token
filterChainDefinitionMap.put("/generic/**", "anon");//pdf预览需要文件
filterChainDefinitionMap.put("/sys/getLoginQrcode/**", "anon"); //登录二维码
filterChainDefinitionMap.put("/sys/getQrcodeToken/**", "anon"); //监听扫码
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
// 代码逻辑说明: 排除静态资源后缀
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
filterChainDefinitionMap.put("/**/*.css", "anon");
filterChainDefinitionMap.put("/**/*.html", "anon");
filterChainDefinitionMap.put("/**/*.svg", "anon");
filterChainDefinitionMap.put("/**/*.pdf", "anon");
filterChainDefinitionMap.put("/**/*.jpg", "anon");
filterChainDefinitionMap.put("/**/*.png", "anon");
filterChainDefinitionMap.put("/**/*.gif", "anon");
filterChainDefinitionMap.put("/**/*.ico", "anon");
filterChainDefinitionMap.put("/**/*.ttf", "anon");
filterChainDefinitionMap.put("/**/*.woff", "anon");
filterChainDefinitionMap.put("/**/*.woff2", "anon");
filterChainDefinitionMap.put("/**/*.glb", "anon");
filterChainDefinitionMap.put("/**/*.wasm", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger**/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v3/**", "anon");
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
//积木报表排除
filterChainDefinitionMap.put("/jmreport/**", "anon");
filterChainDefinitionMap.put("/**/*.js.map", "anon");
filterChainDefinitionMap.put("/**/*.css.map", "anon");
//积木BI大屏和仪表盘排除
filterChainDefinitionMap.put("/drag/view", "anon");
filterChainDefinitionMap.put("/drag/page/queryById", "anon");
filterChainDefinitionMap.put("/drag/page/addVisitsNumber", "anon");
filterChainDefinitionMap.put("/drag/page/queryTemplateList", "anon");
filterChainDefinitionMap.put("/drag/share/view/**", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getMapDataByCode", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/queryAllById", "anon");
filterChainDefinitionMap.put("/jimubi/view", "anon");
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
//大屏模板例子
filterChainDefinitionMap.put("/test/bigScreen/**", "anon");
filterChainDefinitionMap.put("/bigscreen/template1/**", "anon");
filterChainDefinitionMap.put("/bigscreen/template2/**", "anon");
//filterChainDefinitionMap.put("/test/jeecgDemo/rabbitMqClientTest/**", "anon"); //MQ测试
//filterChainDefinitionMap.put("/test/jeecgDemo/html", "anon"); //模板页面
//filterChainDefinitionMap.put("/test/jeecgDemo/redis/**", "anon"); //redis测试
//websocket排除
filterChainDefinitionMap.put("/websocket/**", "anon");//系统通知和公告
filterChainDefinitionMap.put("/newsWebsocket/**", "anon");//CMS模块
filterChainDefinitionMap.put("/vxeSocket/**", "anon");//JVxeTable无痕刷新示例
//App vue3版本查询版本接口
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//仪表盘(按钮通信)
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
//App vue3版本查询版本接口
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//性能监控——安全隐患泄露TOEKNdurid连接池也有
//filterChainDefinitionMap.put("/actuator/**", "anon");
//测试模块排除
filterChainDefinitionMap.put("/test/seata/**", "anon");
//错误路径排除
filterChainDefinitionMap.put("/error", "anon");
// 企业微信证书排除
filterChainDefinitionMap.put("/WW_verify*", "anon");
filterChainDefinitionMap.put("/openapi/call/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
filterMap.put("jwt", new JwtFilter(cloudServer==null));
shiroFilterFactoryBean.setFilters(filterMap);
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
// 未授权界面返回JSON
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* spring过滤装饰器 <br/>
* 因为shiro的filter不支持异步请求,导致所有的异步请求都会报错. <br/>
* 所以需要用spring的FilterRegistrationBean再代理一下shiro的filter.为他扩展异步支持. <br/>
* 后续所有异步的接口都需要再这里增加registration.addUrlPatterns("/xxx/xxx");
* @return
* @author chenrui
* @date 2024/12/3 19:49
*/
@Bean
public FilterRegistrationBean shiroFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
registration.setEnabled(true);
// 代码逻辑说明: [issues/7491]运行耗时长,效率慢
registration.addUrlPatterns("/test/ai/chat/send");
registration.addUrlPatterns("/airag/flow/run");
registration.addUrlPatterns("/airag/flow/debug");
registration.addUrlPatterns("/airag/chat/send");
registration.addUrlPatterns("/airag/app/debug");
registration.addUrlPatterns("/airag/app/prompt/generate");
registration.addUrlPatterns("/airag/chat/receive/**");
// 添加SSE接口的异步支持
registration.addUrlPatterns("/airag/extData/evaluator/debug");
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateChartSse");
registration.addUrlPatterns("/drag/onlDragDatasetHead/updateChartOptSse");
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateSqlSse");
//支持异步
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
return registration;
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
/*
* 关闭shiro自带的session详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//自定义缓存实现,使用redis
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
/**
* 下面的代码是添加注解支持
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
/**
* 解决重复代理问题 github#994
* 添加前缀判断 不匹配 任何Advisor
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
return defaultAdvisorAutoProxyCreator;
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisCacheManager redisCacheManager() {
log.info("===============(1)创建缓存管理器RedisCacheManager");
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
//redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
redisCacheManager.setPrincipalIdFieldName("id");
//用户权限信息缓存时间
redisCacheManager.setExpire(200000);
return redisCacheManager;
}
/**
* RedisConfig在项目starter项目中
* jeecg-boot-starter-github\jeecg-boot-common\src\main\java\org\jeecg\common\modules\redis\config\RedisConfig.java
*
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*
* @return
*/
@Bean
public IRedisManager redisManager() {
log.info("===============(2)创建RedisManager,连接Redis..");
IRedisManager manager;
// sentinel cluster redis【issues/5569】shiro集成 redis 不支持 sentinel 方式部署的redis集群 #5569
if (Objects.nonNull(redisProperties)
&& Objects.nonNull(redisProperties.getSentinel())
&& !CollectionUtils.isEmpty(redisProperties.getSentinel().getNodes())) {
RedisSentinelManager sentinelManager = new RedisSentinelManager();
sentinelManager.setMasterName(redisProperties.getSentinel().getMaster());
sentinelManager.setHost(String.join(",", redisProperties.getSentinel().getNodes()));
sentinelManager.setPassword(redisProperties.getPassword());
sentinelManager.setDatabase(redisProperties.getDatabase());
return sentinelManager;
}
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
RedisManager redisManager = new RedisManager();
redisManager.setHost(lettuceConnectionFactory.getHostName() + ":" + lettuceConnectionFactory.getPort());
//(lettuceConnectionFactory.getPort());
redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
redisManager.setTimeout(0);
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
redisManager.setPassword(lettuceConnectionFactory.getPassword());
}
manager = redisManager;
}else{
// redis集群支持优先使用集群配置
RedisClusterManager redisManager = new RedisClusterManager();
Set<HostAndPort> portSet = new HashSet<>();
lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
//update-begin--Author:scott Date:20210531 for修改集群模式下未设置redis密码的bug issues/I3QNIC
if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
redisManager.setPassword(lettuceConnectionFactory.getPassword());
redisManager.setJedisCluster(jedisCluster);
} else {
JedisCluster jedisCluster = new JedisCluster(portSet);
redisManager.setJedisCluster(jedisCluster);
}
manager = redisManager;
}
return manager;
}
/**
* 解决 ShiroRequestMappingConfig 获取 requestMappingHandlerMapping Bean 冲突
* spring-boot-autoconfigure:3.4.5 和 spring-boot-actuator-autoconfigure:3.4.5
*/
@Primary
@Bean
public RequestMappingHandlerMapping overridedRequestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
return mapping;
}
private List<String> rebuildUrl(String[] bases, String[] uris) {
List<String> urls = new ArrayList<>();
for (String base : bases) {
for (String uri : uris) {
urls.add(prefix(base)+prefix(uri));
}
}
return urls;
}
private String prefix(String seg) {
return seg.startsWith("/") ? seg : "/"+seg;
}
}

View File

@ -1,239 +0,0 @@
package org.jeecg.config.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Set;
/**
* @Description: 用户登录鉴权和获取用户授权
* @Author: Scott
* @Date: 2019-4-23 8:13
* @Version: 1.1
*/
@Component
@Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ShiroRealm extends AuthorizingRealm {
@Lazy
@Resource
private CommonAPI commonApi;
@Lazy
@Resource
private RedisUtil redisUtil;
/**
* 必须重写此方法不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
* 触发检测用户权限时才会调用此方法例如checkRole,checkPermission
*
* @param principals 身份信息
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
String username = null;
String userId = null;
if (principals != null) {
LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
username = sysUser.getUsername();
userId = sysUser.getId();
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 设置用户拥有的角色集合比如“admin,test”
Set<String> roleSet = commonApi.queryUserRolesById(userId);
//System.out.println(roleSet.toString());
info.setRoles(roleSet);
// 设置用户拥有的权限集合比如“sys:role:add,sys:user:add”
Set<String> permissionSet = commonApi.queryUserAuths(userId);
info.addStringPermissions(permissionSet);
//System.out.println(permissionSet);
log.debug("===============Shiro权限认证成功==============");
return info;
}
/**
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
* 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
*
* @param auth 用户登录的账号密码信息
* @return 返回封装了用户信息的 AuthenticationInfo 实例
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
String token = (String) auth.getCredentials();
if (token == null) {
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +"URL:"+req.getRequestURI());
throw new AuthenticationException("token为空!");
}
// 校验token有效性
LoginUser loginUser = null;
try {
loginUser = this.checkUserTokenIsEffect(token);
} catch (AuthenticationException e) {
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
// 重新抛出异常让JwtFilter统一处理避免返回两次错误响应
throw e;
}
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
/**
* 校验token的有效性
*
* @param token
*/
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
// 解密获得username用于和数据库进行对比
String username = JwtUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("Token非法无效!");
}
// 查询用户信息
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
//LoginUser loginUser = commonApi.getUserByName(username);
if (loginUser == null) {
throw new AuthenticationException("用户不存在!");
}
// 判断用户状态
if (loginUser.getStatus() != 1) {
throw new AuthenticationException("账号已被锁定,请联系管理员!");
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
// 用户登录Token过期提示信息
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
throw new AuthenticationException(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
}
// 代码逻辑说明: 校验用户的tenant_id和前端传过来的是否一致
String userTenantIds = loginUser.getRelTenantIds();
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
String contextTenantId = TenantContext.getTenant();
log.debug("登录租户:" + contextTenantId);
log.debug("用户拥有那些租户:" + userTenantIds);
//登录用户无租户前端header中租户ID值为 0
String str ="0";
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
// 代码逻辑说明: /issues/I4O14W 用户租户信息变更判断漏洞
String[] arr = userTenantIds.split(",");
if(!oConvertUtils.isIn(contextTenantId, arr)){
boolean isAuthorization = false;
//========================================================================
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
redisUtil.del(loginUserKey);
LoginUser loginUserFromDb = commonApi.getUserByName(username);
if (oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
if (oConvertUtils.isIn(contextTenantId, newArray)) {
isAuthorization = true;
}
}
//========================================================================
//*********************************************
if(!isAuthorization){
log.info("租户异常——登录租户:" + contextTenantId);
log.info("租户异常——用户拥有租户组:" + userTenantIds);
throw new AuthenticationException("登录租户授权变更,请重新登陆!");
}
//*********************************************
}
}
}
return loginUser;
}
/**
* JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
* 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)缓存有效期设置为Jwt有效时间的2倍
* 2、当该用户再次请求时通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
* 3、当该用户这次请求jwt生成的token值已经超时但该token对应cache中的k还是存在则表示该用户一直在操作只是JWT的token失效了程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值该缓存生命周期重新计算
* 4、当该用户这次请求jwt在生成的token值已经超时并在cache中不存在对应的k则表示该用户账户空闲超时返回用户信息已失效请重新登录。
* 注意: 前端请求Header中设置Authorization保持不变校验有效性以缓存中的token为准。
* 用户过期时间 = Jwt有效时间 * 2。
*
* @param userName
* @param passWord
* @return
*/
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
// 从token中解析客户端类型保持续期时使用相同的客户端类型
String clientType = JwtUtil.getClientType(token);
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
// 根据客户端类型设置对应的缓存有效时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
: JwtUtil.EXPIRE_TIME * 2 / 1000;
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
log.debug("——————————用户在线操作更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
}
// else {
// // 设置超时时间
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
// }
return true;
}
//redis中不存在此TOEKN说明token非法返回false
return false;
}
/**
* 清除当前用户的权限认证缓存
*
* @param principals 权限信息
*/
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
// 代码逻辑说明: 【TV360X-1320】分配权限必须退出重新登录才生效造成很多用户困扰---
super.clearCachedAuthorizationInfo(principals);
}
}

View File

@ -1,77 +0,0 @@
package org.jeecg.config.shiro.filters;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.beans.factory.BeanInitializationException;
import jakarta.servlet.Filter;
import java.util.Map;
/**
* 自定义ShiroFilterFactoryBean解决资源中文路径问题
* @author: jeecg-boot
*/
@Slf4j
public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean {
@Override
public Class getObjectType() {
return MySpringShiroFilter.class;
}
@Override
protected AbstractShiroFilter createInstance() throws Exception {
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
FilterChainManager manager = createFilterChainManager();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// do not know about FilterChainManagers - only resolvers:
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
Map<String, Filter> filterMap = manager.getFilters();
Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
if (invalidRequestFilter instanceof InvalidRequestFilter) {
//此处是关键,设置false跳过URL携带中文400servletPath中文校验bug
((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
}
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//injection of the SecurityManager and FilterChainResolver:
return new MySpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
private static final class MySpringShiroFilter extends AbstractShiroFilter {
protected MySpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
if (webSecurityManager == null) {
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
} else {
this.setSecurityManager(webSecurityManager);
if (resolver != null) {
this.setFilterChainResolver(resolver);
}
}
}
}
}

View File

@ -1,132 +0,0 @@
package org.jeecg.config.shiro.filters;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.shiro.JwtToken;
import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* @Description: 鉴权登录拦截器
* @Author: Scott
* @Date: 2018/10/7
**/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 默认开启跨域设置(使用单体)
* 微服务情况下此属性设置为false
*/
private boolean allowOrigin = true;
public JwtFilter(){}
public JwtFilter(boolean allowOrigin){
this.allowOrigin = allowOrigin;
}
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
// 判断当前路径是不是注解了@IngoreAuth路径如果是则放开验证
if (InMemoryIgnoreAuth.contains(((HttpServletRequest) request).getServletPath())) {
return true;
}
executeLogin(request, response);
return true;
} catch (Exception e) {
// 使用异常中的具体错误信息,保留"不允许同一账号多地同时登录"等具体提示
String errorMsg = e.getMessage();
if (oConvertUtils.isEmpty(errorMsg)) {
errorMsg = CommonConstant.TOKEN_IS_INVALID_MSG;
}
JwtUtil.responseError((HttpServletResponse)response, 401, errorMsg);
return false;
}
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
// 代码逻辑说明: JT-355 OA聊天添加token验证获取token参数
if (oConvertUtils.isEmpty(token)) {
token = httpServletRequest.getParameter("token");
}
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功返回true
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if(allowOrigin){
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN));
// 允许客户端请求方法
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,OPTIONS,PUT,DELETE");
// 允许客户端提交的Header
String requestHeaders = httpServletRequest.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
if (StringUtils.isNotEmpty(requestHeaders)) {
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
}
// 允许客户端携带凭证信息(是否允许发送Cookie)
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}
// 跨域时会首先发送一个option请求这里我们给option请求直接返回正常状态
if (RequestMethod.OPTIONS.name().equalsIgnoreCase(httpServletRequest.getMethod())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
// 代码逻辑说明: 多租户用到
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
TenantContext.setTenant(tenantId);
return super.preHandle(request, response);
}
/**
* JwtFilter中ThreadLocal需要及时清除 #3634
*
* @param request
* @param response
* @param exception
* @throws Exception
*/
@Override
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
//log.info("------清空线程中多租户的ID={}------",TenantContext.getTenant());
TenantContext.clear();
}
}

View File

@ -1,67 +0,0 @@
package org.jeecg.config.shiro.filters;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import lombok.extern.slf4j.Slf4j;
/**
* @Author Scott
* @create 2019-02-01 15:56
* @desc 鉴权请求URL访问权限拦截器
*/
@Slf4j
public class ResourceCheckFilter extends AccessControlFilter {
private String errorUrl;
public String getErrorUrl() {
return errorUrl;
}
public void setErrorUrl(String errorUrl) {
this.errorUrl = errorUrl;
}
/**
* 表示是否允许访问 如果允许访问返回true否则false
*
* @param servletRequest
* @param servletResponse
* @param o 表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
* @return
* @throws Exception
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
String url = getPathWithinApplication(servletRequest);
log.info("当前用户正在访问的 url => " + url);
return subject.isPermitted(url);
}
/**
* onAccessDenied表示当访问拒绝时是否已经处理了 如果返回 true 表示需要继续处理; 如果返回 false
* 表示该拦截器实例已经处理了,将直接返回即可。
*
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
log.info("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.sendRedirect(request.getContextPath() + this.errorUrl);
// 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
return false;
}
}

View File

@ -1,32 +0,0 @@
package org.jeecg.config.sign.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 签名校验注解
* 用于方法级别的签名验证功能等同于yml中的jeecg.signUrls配置
* 参考DragSignatureAspect的设计思路使用AOP切面实现
*
* @author GitHub Copilot
* @since 2025-12-15
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignatureCheck {
/**
* 是否启用签名校验
* @return true-启用(默认), false-禁用
*/
boolean enabled() default true;
/**
* 签名校验失败时的错误消息
* @return 错误消息
*/
String errorMessage() default "Sign签名校验失败";
}

View File

@ -1,112 +0,0 @@
package org.jeecg.config.sign.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.config.sign.annotation.SignatureCheck;
import org.jeecg.config.sign.interceptor.SignAuthInterceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestBody;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 基于AOP的签名验证切面
* 复用SignAuthInterceptor的成熟签名验证逻辑
*
* @author GitHub Copilot
* @since 2025-12-15
*/
@Aspect
@Slf4j
@Component("signatureCheckAspect")
public class SignatureCheckAspect {
/**
* 复用SignAuthInterceptor的签名验证逻辑
*/
private final SignAuthInterceptor signAuthInterceptor = new SignAuthInterceptor();
/**
* 验签切点:拦截所有标记了@SignatureCheck注解的方法
*/
@Pointcut("@annotation(org.jeecg.config.sign.annotation.SignatureCheck)")
private void signatureCheckPointCut() {
}
/**
* 开始验签
*/
@Before("signatureCheckPointCut()")
public void doSignatureValidation(JoinPoint point) throws Exception {
// 获取方法上的注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
SignatureCheck signatureCheck = method.getAnnotation(SignatureCheck.class);
log.info("AOP签名验证: {}.{}", method.getDeclaringClass().getSimpleName(), method.getName());
// 如果注解被禁用,直接返回
if (!signatureCheck.enabled()) {
log.info("签名验证已禁用,跳过");
return;
}
// update-begin---author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数解决签名校验时读取请求体为空的问题
Object bodyParam = null;
Object[] args = point.getArgs();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
Annotation[] annotations = parameterAnnotations[i];
boolean hasRequestBodyAnnotation = Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType().equals(RequestBody.class));
if (hasRequestBodyAnnotation) {
// 捕获携带@RequestBody注解的参数供签名校验使用
bodyParam = arg;
}
}
// update-end-----author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数解决签名校验时读取请求体为空的问题
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
log.error("无法获取请求上下文");
throw new IllegalArgumentException("无法获取请求上下文");
}
HttpServletRequest request = attributes.getRequest();
log.info("X-SIGN: {}, X-TIMESTAMP: {}", request.getHeader("X-SIGN"), request.getHeader("X-TIMESTAMP"));
try {
// 直接调用SignAuthInterceptor的验证逻辑
signAuthInterceptor.validateSignature(request, bodyParam);
log.info("AOP签名验证通过");
} catch (IllegalArgumentException e) {
// 使用注解中配置的错误消息,或者保留原始错误消息
String errorMessage = signatureCheck.errorMessage();
log.error("AOP签名验证失败: {}", e.getMessage());
if ("Sign签名校验失败".equals(errorMessage)) {
// 如果是默认错误消息,使用原始的详细错误信息
throw e;
} else {
// 如果是自定义错误消息,使用自定义消息
throw new IllegalArgumentException(errorMessage, e);
}
} catch (Exception e) {
// 包装其他异常
String errorMessage = signatureCheck.errorMessage();
log.error("AOP签名验证异常: {}", e.getMessage());
throw new IllegalArgumentException(errorMessage, e);
}
}
}

View File

@ -1,7 +1,6 @@
package org.jeecg.config.sign.interceptor; package org.jeecg.config.sign.interceptor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.TenantConstant;
import org.jeecg.common.util.PathMatcherUtil; import org.jeecg.common.util.PathMatcherUtil;
import org.jeecg.config.JeecgBaseConfig; import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.filter.RequestBodyReserveFilter; import org.jeecg.config.filter.RequestBodyReserveFilter;
@ -65,8 +64,6 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
//------------------------------------------------------------ //------------------------------------------------------------
// 建议此处只添加post请求地址而不是所有的都需要走过滤器 // 建议此处只添加post请求地址而不是所有的都需要走过滤器
registration.addUrlPatterns(signUrlsArray); registration.addUrlPatterns(signUrlsArray);
// 增加注解签名请求
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
return registration; return registration;
} }

View File

@ -33,104 +33,63 @@ public class SignAuthInterceptor implements HandlerInterceptor {
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("签名拦截器 Interceptor request URI = " + request.getRequestURI()); log.debug("Sign Interceptor request URI = " + request.getRequestURI());
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
//对参数进行签名验证
String headerSign = request.getHeader(CommonConstant.X_SIGN);
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
try { if(oConvertUtils.isEmpty(xTimestamp)){
// 调用验证逻辑 Result<?> result = Result.error("Sign签名校验失败时间戳为空");
validateSignature(request); log.error("Sign 签名校验失败Header xTimestamp 为空");
return true; //校验失败返回前端
} catch (IllegalArgumentException e) {
// 验证失败,返回错误响应
log.error("Sign 签名校验失败!{}", e.getMessage());
response.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8"); response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter(); PrintWriter out = response.getWriter();
Result<?> result = Result.error(e.getMessage()); out.print(JSON.toJSON(result));
return false;
}
//客户端时间
Long clientTimestamp = Long.parseLong(xTimestamp);
int length = 14;
int length1000 = 1000;
//1.校验签名时间兼容X_TIMESTAMP的新老格式
if (xTimestamp.length() == length) {
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子20220308152143)
if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) {
log.error("签名验证失败:X-TIMESTAMP已过期注意系统时间和服务器时间是否有误差");
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
}
} else {
//b. X_TIMESTAMP格式是 时间戳 (例子1646552406000)
if ((System.currentTimeMillis() - clientTimestamp) > (MAX_EXPIRE * length1000)) {
log.error("签名验证失败:X-TIMESTAMP已过期注意系统时间和服务器时间是否有误差");
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
}
}
//2.校验签名
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
if (isSigned) {
log.debug("Sign 签名通过Header Sign : {}",headerSign);
return true;
} else {
log.debug("sign allParams: {}", allParams);
log.error("request URI = " + request.getRequestURI());
log.error("Sign 签名校验失败Header Sign : {}",headerSign);
//校验失败返回前端
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
Result<?> result = Result.error("Sign签名校验失败");
out.print(JSON.toJSON(result)); out.print(JSON.toJSON(result));
return false; return false;
} }
} }
/**
* 签名验证核心逻辑
* 提取出来供AOP切面复用
* @param request HTTP请求
* @throws IllegalArgumentException 验证失败时抛出异常
*/
public void validateSignature(HttpServletRequest request) throws IllegalArgumentException {
validateSignature(request, null);
}
/**
* 签名验证核心逻辑
* 提取出来供AOP切面复用
* @param request HTTP请求
* @throws IllegalArgumentException 验证失败时抛出异常
*/
public void validateSignature(HttpServletRequest request, Object bodyParam) throws IllegalArgumentException {
try {
log.debug("开始签名验证: {} {}", request.getMethod(), request.getRequestURI());
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
log.debug("提取参数: {}", allParams);
//对参数进行签名验证
String headerSign = request.getHeader(CommonConstant.X_SIGN);
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
if(oConvertUtils.isEmpty(xTimestamp)){
log.error("Sign签名校验失败时间戳为空");
throw new IllegalArgumentException("Sign签名校验失败请求参数不完整");
}
//客户端时间
Long clientTimestamp = Long.parseLong(xTimestamp);
int length = 14;
int length1000 = 1000;
//1.校验签名时间兼容X_TIMESTAMP的新老格式
if (xTimestamp.length() == length) {
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子20220308152143)
long currentTimestamp = DateUtils.getCurrentTimestamp();
long timeDiff = currentTimestamp - clientTimestamp;
log.debug("时间戳验证(yyyyMMddHHmmss): 时间差{}秒", timeDiff);
if (timeDiff > MAX_EXPIRE) {
log.error("时间戳已过期: {}秒 > {}秒", timeDiff, MAX_EXPIRE);
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
}
} else {
//b. X_TIMESTAMP格式是 时间戳 (例子1646552406000)
long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - clientTimestamp;
long maxExpireMs = MAX_EXPIRE * length1000;
log.debug("时间戳验证(Unix): 时间差{}ms", timeDiff);
if (timeDiff > maxExpireMs) {
log.error("时间戳已过期: {}ms > {}ms", timeDiff, maxExpireMs);
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
}
}
//2.校验签名
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
if (isSigned) {
log.debug("签名验证通过");
} else {
log.error("签名验证失败, 参数: {}", allParams);
throw new IllegalArgumentException("Sign签名校验失败");
}
} catch (IllegalArgumentException e) {
// 重新抛出签名验证异常
throw e;
} catch (Exception e) {
// 包装其他异常如IOException
log.error("签名验证异常: {}", e.getMessage());
throw new IllegalArgumentException("Sign签名校验失败" + e.getMessage());
}
}
} }

View File

@ -35,7 +35,7 @@ public class HttpUtils {
* @date 20210621 * @date 20210621
* @param request * @param request
*/ */
public static SortedMap<String, String> getAllParams(HttpServletRequest request, Object bodyParam) throws IOException { public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
SortedMap<String, String> result = new TreeMap<>(); SortedMap<String, String> result = new TreeMap<>();
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username // 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
@ -65,13 +65,7 @@ public class HttpUtils {
Map<String, String> allRequestParam = new HashMap<>(16); Map<String, String> allRequestParam = new HashMap<>(16);
// get请求不需要拿body参数 // get请求不需要拿body参数
if (!HttpMethod.GET.name().equals(request.getMethod())) { if (!HttpMethod.GET.name().equals(request.getMethod())) {
if (bodyParam != null) { allRequestParam = getAllRequestParam(request);
// update-begin---author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
allRequestParam = JSONObject.parseObject(JSONObject.toJSONString(bodyParam), Map.class);
// update-end-----author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
} else {
allRequestParam = getAllRequestParam(request);
}
} }
// 将URL的参数和body参数进行合并 // 将URL的参数和body参数进行合并
if (allRequestParam != null) { if (allRequestParam != null) {

View File

@ -11,12 +11,7 @@ import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.TreeMap;
/** /**
* 签名工具类 * 签名工具类
@ -54,7 +49,12 @@ public class SignUtil {
String paramsJsonStr = JSONObject.toJSONString(params); String paramsJsonStr = JSONObject.toJSONString(params);
log.debug("Param paramsJsonStr : {}", paramsJsonStr); log.debug("Param paramsJsonStr : {}", paramsJsonStr);
//设置签名秘钥 //设置签名秘钥
String signatureSecret = SignUtil.getSignatureSecret(); JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
if(oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)){
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 ");
}
try { try {
//【issues/I484RW】2.4.6部署后下拉搜索框提示“sign签名检验失败” //【issues/I484RW】2.4.6部署后下拉搜索框提示“sign签名检验失败”
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase(); return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
@ -63,129 +63,4 @@ public class SignUtil {
return null; return null;
} }
} }
/**
* 通过前端签名算法生成签名
*
* @param url 请求的完整URL包含查询参数
* @param requestParams 使用 @RequestParam 获取的参数集合
* @param requestBodyParams 使用 @RequestBody 获取的参数集合
* @return 计算得到的签名大写MD5若参数不足返回 null
*/
public static String generateRequestSign(String url, Map<String, Object> requestParams, Map<String, Object> requestBodyParams) {
if (oConvertUtils.isEmpty(url)) {
return null;
}
// 解析URL上的查询参数与路径变量
Map<String, String> urlParams = parseQueryString(url);
// 合并URL参数与@RequestParam参数确保数值和布尔类型转换为字符串
Map<String, String> mergedParams = mergeObject(urlParams, requestParams);
// 按需合并@RequestBody参数
if (requestBodyParams != null && !requestBodyParams.isEmpty()) {
mergedParams = mergeObject(mergedParams, requestBodyParams);
}
// 按键名升序排序,保持与前端一致的签名顺序
SortedMap<String, String> sortedParams = new TreeMap<>(mergedParams);
// 去除时间戳字段,避免参与签名
sortedParams.remove("_t");
// 序列化为JSON字符串
String paramsJsonStr = JSONObject.toJSONString(sortedParams);
// 读取签名秘钥
String signatureSecret = getSignatureSecret();
// 计算MD5摘要并转大写
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes(StandardCharsets.UTF_8)).toUpperCase();
}
/**
* 解析URL中的查询参数并处理末尾逗号分隔的路径变量片段。
*
* @param url 请求的完整URL
* @return 解析后的参数映射,数值与布尔类型均转换为字符串
*/
private static Map<String, String> parseQueryString(String url) {
Map<String, String> result = new HashMap<>(16);
int fragmentIndex = url.indexOf('#');
if (fragmentIndex >= 0) {
url = url.substring(0, fragmentIndex);
}
int questionIndex = url.indexOf('?');
String paramString = null;
if (questionIndex >= 0 && questionIndex < url.length() - 1) {
paramString = url.substring(questionIndex + 1);
}
// 处理路径变量末尾以逗号分隔的段,例如 /sys/dict/getDictItems/sys_user,realname,username
int lastSlashIndex = url.lastIndexOf(SymbolConstant.SINGLE_SLASH);
if (lastSlashIndex >= 0 && lastSlashIndex < url.length() - 1) {
String lastPathVariable = url.substring(lastSlashIndex + 1);
int qIndexInPath = lastPathVariable.indexOf('?');
if (qIndexInPath >= 0) {
lastPathVariable = lastPathVariable.substring(0, qIndexInPath);
}
if (lastPathVariable.contains(SymbolConstant.COMMA)) {
String decodedPathVariable = URLDecoder.decode(lastPathVariable, StandardCharsets.UTF_8);
result.put(X_PATH_VARIABLE, decodedPathVariable);
}
}
if (oConvertUtils.isNotEmpty(paramString)) {
String[] pairs = paramString.split(SymbolConstant.AND);
for (String pair : pairs) {
int equalIndex = pair.indexOf('=');
if (equalIndex > 0 && equalIndex < pair.length() - 1) {
String key = pair.substring(0, equalIndex);
String value = pair.substring(equalIndex + 1);
// 解码并统一类型为字符串
String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);
result.put(decodedKey, decodedValue);
}
}
}
return result;
}
/**
* 合并两个参数映射,并保证数值与布尔类型统一转为字符串。
*
* @param target 初始参数映射
* @param source 待合并的参数映射
* @return 合并后的新映射
*/
private static Map<String, String> mergeObject(Map<String, String> target, Map<String, Object> source) {
Map<String, String> merged = new HashMap<>(16);
if (target != null && !target.isEmpty()) {
merged.putAll(target);
}
if (source != null && !source.isEmpty()) {
for (Map.Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof Number) {
// 数值类型转字符串,保持前后端一致
merged.put(key, String.valueOf(value));
} else if (value instanceof Boolean) {
// 布尔类型转字符串,保持前后端一致
merged.put(key, String.valueOf(value));
} else if (value != null) {
merged.put(key, String.valueOf(value));
}
}
}
return merged;
}
/**
* 读取并校验签名秘钥配置。
*
* @return 有效的签名秘钥
*/
private static String getSignatureSecret() {
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
if (oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)) {
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 ");
}
return signatureSecret;
}
} }

View File

@ -19,10 +19,6 @@ public class Firewall {
* 低代码模式dev:开发模式prod:发布模式——关闭所有在线开发配置能力) * 低代码模式dev:开发模式prod:发布模式——关闭所有在线开发配置能力)
*/ */
private String lowCodeMode; private String lowCodeMode;
/**
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
*/
private Boolean isConcurrent = true;
/** /**
* 是否开启默认密码登录提醒true 登录后提示必须修改默认密码) * 是否开启默认密码登录提醒true 登录后提示必须修改默认密码)
*/ */
@ -78,12 +74,4 @@ public class Firewall {
public void setDisableSelectAll(Boolean disableSelectAll) { public void setDisableSelectAll(Boolean disableSelectAll) {
this.disableSelectAll = disableSelectAll; this.disableSelectAll = disableSelectAll;
} }
public Boolean getIsConcurrent() {
return isConcurrent;
}
public void setIsConcurrent(Boolean isConcurrent) {
this.isConcurrent = isConcurrent;
}
} }

View File

@ -2,7 +2,7 @@ package org.jeecg.modules.base.service.impl;
import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils; import org.jeecg.common.util.LoginUserUtils;
import org.jeecg.common.api.dto.LogDTO; import org.jeecg.common.api.dto.LogDTO;
import org.jeecg.common.constant.enums.ClientTerminalTypeEnum; import org.jeecg.common.constant.enums.ClientTerminalTypeEnum;
import org.jeecg.common.util.BrowserUtils; import org.jeecg.common.util.BrowserUtils;
@ -74,7 +74,7 @@ public class BaseCommonServiceImpl implements BaseCommonService {
//获取登录用户信息 //获取登录用户信息
if(user==null){ if(user==null){
try { try {
user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); user = LoginUserUtils.getSessionUser();
} catch (Exception e) { } catch (Exception e) {
//e.printStackTrace(); //e.printStackTrace();
} }

View File

@ -1,429 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP Stdio 工具 - 修复编码问题
确保所有输出都使用UTF-8编码
"""
import json
import sys
import os
from typing import Dict, Any
import logging
# 强制使用UTF-8编码
if sys.platform == "win32":
# Windows需要特殊处理
import io
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
else:
# Unix-like系统
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
# 设置环境变量
os.environ['PYTHONIOENCODING'] = 'utf-8'
os.environ['PYTHONUTF8'] = '1'
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
encoding='utf-8'
)
logger = logging.getLogger("mcp-tool")
class FixedMCPServer:
"""修复编码问题的MCP服务器"""
def __init__(self):
self.tools = {}
self.initialize_tools()
def initialize_tools(self):
"""初始化工具集"""
# 获取时间
self.tools["get_time"] = {
"name": "get_time",
"description": "获取当前时间",
"inputSchema": {
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "时间格式",
"enum": ["iso", "timestamp", "human", "chinese"],
"default": "iso"
}
}
}
}
# 文本处理工具
self.tools["text_process"] = {
"name": "text_process",
"description": "文本处理工具",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "输入文本"
},
"operation": {
"type": "string",
"description": "操作类型",
"enum": ["length", "upper", "lower", "reverse", "count_words"],
"default": "length"
}
},
"required": ["text"]
}
}
# 数据格式工具
self.tools["format_data"] = {
"name": "format_data",
"description": "格式化数据",
"inputSchema": {
"type": "object",
"properties": {
"data": {
"type": "string",
"description": "原始数据"
},
"format": {
"type": "string",
"description": "格式类型",
"enum": ["json", "yaml", "xml"],
"default": "json"
}
},
"required": ["data"]
}
}
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
"""处理请求"""
try:
method = request.get("method")
params = request.get("params", {})
if method == "tools/list":
return self.handle_tools_list()
elif method == "tools/call":
return self.handle_tool_call(params)
elif method == "ping":
return {"result": "pong"}
else:
return self.create_error_response(
code=-32601,
message="Method not found"
)
except Exception as e:
logger.error(f"Error handling request: {e}")
return self.create_error_response(
code=-32603,
message=f"Internal error: {str(e)}"
)
def handle_tools_list(self) -> Dict[str, Any]:
"""列出所有工具 - 确保返回标准JSON"""
return {
"result": {
"tools": list(self.tools.values())
}
}
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""调用工具 - 修复响应格式"""
name = params.get("name")
arguments = params.get("arguments", {})
if name not in self.tools:
return self.create_error_response(
code=-32602,
message=f"Tool '{name}' not found"
)
try:
if name == "get_time":
result = self.execute_get_time(arguments)
elif name == "text_process":
result = self.execute_text_process(arguments)
elif name == "format_data":
result = self.execute_format_data(arguments)
else:
return self.create_error_response(
code=-32602,
message="Tool not implemented"
)
# 确保返回正确的MCP响应格式
return self.create_success_response(result)
except Exception as e:
logger.error(f"Tool execution error: {e}")
return self.create_error_response(
code=-32603,
message=f"Tool execution failed: {str(e)}"
)
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""获取时间 - 支持中文"""
from datetime import datetime
try:
format_type = args.get("format", "iso")
now = datetime.now()
if format_type == "iso":
result = now.isoformat()
elif format_type == "timestamp":
result = now.timestamp()
elif format_type == "human":
result = now.strftime("%Y-%m-%d %H:%M:%S")
elif format_type == "chinese":
result = now.strftime("%Y年%m月%d%H时%M分%S秒")
else:
result = now.isoformat()
logger.info(f"当前系统时间:{result}")
return {
"status": "success",
"format": format_type,
"time": result,
"timestamp": now.timestamp(),
"date": now.strftime("%Y-%m-%d"),
"time_12h": now.strftime("%I:%M:%S %p")
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""文本处理"""
try:
text = args.get("text", "")
operation = args.get("operation", "length")
if operation == "length":
result = len(text)
result_str = f"文本长度: {result} 个字符"
elif operation == "upper":
result = text.upper()
result_str = f"大写: {result}"
elif operation == "lower":
result = text.lower()
result_str = f"小写: {result}"
elif operation == "reverse":
result = text[::-1]
result_str = f"反转: {result}"
elif operation == "count_words":
words = len(text.split())
result = words
result_str = f"单词数: {words}"
else:
raise ValueError(f"未知操作: {operation}")
return {
"status": "success",
"operation": operation,
"original_text": text,
"result": result,
"result_str": result_str,
"text_length": len(text)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"operation": args.get("operation", "")
}
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""格式化数据"""
try:
data_str = args.get("data", "")
format_type = args.get("format", "json")
# 尝试解析为JSON
try:
data = json.loads(data_str)
is_json = True
except:
data = data_str
is_json = False
if format_type == "json":
if is_json:
result = json.dumps(data, ensure_ascii=False, indent=2)
else:
# 如果不是JSON包装成JSON
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
elif format_type == "yaml":
import yaml
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
elif format_type == "xml":
# 简单的XML格式化
if isinstance(data, dict):
result = "<data>"
for k, v in data.items():
result += f"\n <{k}>{v}</{k}>"
result += "\n</data>"
else:
result = f"<text>{data}</text>"
else:
result = str(data)
return {
"status": "success",
"format": format_type,
"original": data_str,
"formatted": result,
"length": len(result)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"format": args.get("format", "")
}
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""创建成功响应 - 确保符合MCP规范"""
# 将数据转换为JSON字符串作为文本内容
content_text = json.dumps(data, ensure_ascii=False, indent=2)
return {
"result": {
"content": [
{
"type": "text",
"text": content_text
}
],
"isError": False
}
}
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
"""创建错误响应"""
return {
"error": {
"code": code,
"message": message
}
}
def safe_json_dump(data: Dict[str, Any]) -> str:
"""安全的JSON序列化确保UTF-8编码"""
try:
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
except:
# 如果失败使用ASCII转义
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
def main():
"""主函数 - 修复Stdio通信"""
logger.info("启动MCP Stdio服务器 (修复编码版)...")
server = FixedMCPServer()
# 初始握手消息
init_message = {
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "fixed-mcp-server",
"version": "1.0.0"
}
}
}
# 发送初始化响应
try:
sys.stdout.write(safe_json_dump(init_message) + "\n")
sys.stdout.flush()
except Exception as e:
logger.error(f"发送初始化消息失败: {e}")
return
logger.info("MCP服务器已初始化")
# 主循环
line_num = 0
while True:
try:
line = sys.stdin.readline()
if not line:
logger.info("输入流结束")
break
line = line.strip()
line_num += 1
if not line:
continue
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
try:
request = json.loads(line)
logger.info(f"解析请求: {request.get('method', 'unknown')}")
# 处理请求
response = server.handle_request(request)
response["jsonrpc"] = "2.0"
response["id"] = request.get("id")
# 发送响应
response_json = safe_json_dump(response)
sys.stdout.write(response_json + "\n")
sys.stdout.flush()
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {e}")
error_response = {
"jsonrpc": "2.0",
"error": {
"code": -32700,
"message": f"Parse error at line {line_num}"
},
"id": None
}
sys.stdout.write(safe_json_dump(error_response) + "\n")
sys.stdout.flush()
except KeyboardInterrupt:
logger.info("接收到中断信号")
break
except Exception as e:
logger.error(f"未处理的错误: {e}")
break
logger.info("MCP服务器已停止")
if __name__ == "__main__":
main()

View File

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>org.jeecgframework.boot3</groupId> <groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-module</artifactId> <artifactId>jeecg-boot-module</artifactId>
<version>3.9.1</version> <version>3.9.0</version>
</parent> </parent>
<artifactId>jeecg-boot-module-airag</artifactId> <artifactId>jeecg-boot-module-airag</artifactId>
@ -33,7 +33,7 @@
<properties> <properties>
<kotlin.version>2.2.0</kotlin.version> <kotlin.version>2.2.0</kotlin.version>
<liteflow.version>2.15.0</liteflow.version> <liteflow.version>2.15.0</liteflow.version>
<apache-tika.version>3.2.3</apache-tika.version> <apache-tika.version>2.9.1</apache-tika.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@ -41,14 +41,14 @@
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId> <artifactId>langchain4j-bom</artifactId>
<version>1.9.1</version> <version>1.3.0</version>
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId> <artifactId>langchain4j-community-bom</artifactId>
<version>1.9.1-beta17</version> <version>1.3.0-beta9</version>
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
@ -75,7 +75,7 @@
<dependency> <dependency>
<groupId>org.jeecgframework.boot3</groupId> <groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-aiflow</artifactId> <artifactId>jeecg-aiflow</artifactId>
<version>3.9.1-beta</version> <version>3.9.0.1</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
@ -107,16 +107,16 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.yomahub</groupId> <groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId> <artifactId>liteflow-script-python</artifactId>
<version>${liteflow.version}</version> <version>${liteflow.version}</version>
<scope>runtime</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- end 注意这几个依赖体积较大每个约50MB。若发布时需要使用请将 <scope>provided</scope> 删除 --> <!-- end 注意这几个依赖体积较大每个约50MB。若发布时需要使用请将 <scope>provided</scope> 删除 -->
<!-- aiflow 脚本依赖 --> <!-- aiflow 脚本依赖 -->
<dependency> <dependency>
<groupId>com.yomahub</groupId> <groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId> <artifactId>liteflow-script-groovy</artifactId>
<version>${liteflow.version}</version> <version>${liteflow.version}</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
@ -151,11 +151,6 @@
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId> <artifactId>langchain4j-open-ai</artifactId>
</dependency> </dependency>
<!-- langChain4j mcp support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>
<dependency> <dependency>
<groupId>dev.langchain4j</groupId> <groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId> <artifactId>langchain4j-ollama</artifactId>
@ -202,11 +197,7 @@
<artifactId>langchain4j-pgvector</artifactId> <artifactId>langchain4j-pgvector</artifactId>
<version>1.3.0-beta9</version> <version>1.3.0-beta9</version>
</dependency> </dependency>
<!-- langChain4j Document Parser 适用于excel、ppt、word --> <!-- langChain4j Document Parser -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.apache.tika</groupId> <groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId> <artifactId>tika-core</artifactId>
@ -233,12 +224,7 @@
<artifactId>tika-parser-text-module</artifactId> <artifactId>tika-parser-text-module</artifactId>
<version>${apache-tika.version}</version> <version>${apache-tika.version}</version>
</dependency> </dependency>
<!-- word模版引擎 -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -44,19 +44,4 @@ public class AiAppConsts {
*/ */
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs"; public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
/**
* 是否开启记忆
*/
public static final Integer IZ_OPEN_MEMORY = 1;
/**
* 会话标题最大长度
*/
public static final int CONVERSATION_MAX_TITLE_LENGTH = 10;
/**
* AI写作的应用id
*/
public static final String WRITER_APP_ID = "2010634128233779202";
} }

View File

@ -104,68 +104,4 @@ public class Prompts {
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" + " - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度低于0.7时启动重写\"\n" + " - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度低于0.7时启动重写\"\n" +
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\""; " - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
/**
* 提示词生成角色及通用要求
*/
public static final String GENERATE_GUIDE_HEADER = "# 角色\n" +
"你是一位AI提示词专家请根据提供的配置信息生成针对AI智能体的“使用指南”提示词。\n" +
"\n" +
"## 通用要求\n" +
"1. 生成的内容将作为系统提示词的一部分。\n" +
"2. **严禁**包含任何角色设定开场白(如“你是一个...AI助手”、“在对话过程中...”等)。\n" +
"3. **只输出提示词内容**不要包含任何解释、寒暄或Markdown代码块标记。\n" +
"4. 语气专业、清晰、指令性强。\n" +
"5. 说明内容请使用中文。\n\n";
/**
* 变量生成提示词
*/
public static final String GENERATE_VAR_PART = "## 任务:生成变量使用指南\n" +
"### 输入信息\n" +
"**变量列表**\n" +
"%s\n" +
"### 要求\n" +
"1. 请生成一段**变量使用指南**。\n" +
"2. **遍历生成**:请遍历【输入信息】中的所有变量,为**每一个**变量生成一条具体的使用指南。\n" +
"3. **格式要求**:请仿照以下句式,根据变量的实际含义生成(确保包含{{变量名}}\n" +
" 例如针对name变量 -> “回复问题时,请称呼你的用户为{{name}}。”\n" +
" 例如针对age变量 -> “用户的年龄是{{age}},请在对话中适时使用。”\n" +
" 例如:针对其他变量 -> “用户的[变量描述]是{{[变量名]}},请在对话中适时使用。”\n" +
"4. **通用更新指令**请在变量指南的最后单独生成一条指令明确指示AI“当从用户对话中获取到上述变量<列出所有变量名,用顿号分隔>)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。”\n" +
"5. **保留原文**:如果输入信息中包含具体的行为指令(如“回复问题时,请称呼你的用户为{{name}}”),请在生成的指南中**直接引用原文**,不要进行改写或格式化,以免改变用户的原意。\n\n";
/**
* 记忆库生成提示词
*/
public static final String GENERATE_MEMORY_PART = "## 任务:生成记忆库使用指南\n" +
"### 输入信息\n" +
"**记忆库描述**\n" +
"%s\n" +
"### 要求\n" +
"1. 请生成一段**记忆库使用指南**,加入【工具使用强制协议】:\n" +
" - **全自动存储(无需用户指令)**:你必须时刻像一个观察者一样分析对话。一旦检测到符合记忆库描述的信息(尤其是:**姓名、职业、年龄**、联系方式、偏好、经历等),**立即**调用 `add_memory` 工具存储。**绝对不要**询问用户是否需要存储,也不要等待用户明确指令。这是你的后台职责。\n" +
" - **全自动检索(强制优先)**\n" +
" * **禁止直接反问**:当用户提出依赖个人信息的问题(如“推荐适合我的...”或“我之前说过...”)时,**绝对禁止**直接反问用户“你的爱好是什么?”。\n" +
" * **必须先查后答**:你必须**先假设**记忆库中已经有了答案,并**立即调用** `query_memory` 进行验证。只有当工具返回“未找到相关信息”后,你才有资格询问用户。\n" +
" * **宁可查空,不可不查**:即使你觉得可能没有记录,也必须先走一遍查询流程。\n" +
" - **动态调整**:请根据【输入信息】中提供的**记忆库状态描述**,明确界定哪些信息属于“自动捕获”的范围。\n" +
" - **行为准则**\n" +
" * 你的记忆动作应该是**主动且无感**的。用户只负责聊天,你负责记住一切重要细节。\n" +
" * **禁止口头空谈**:严禁只回复“我知道了”、“已记住”而实际不调用工具。这是严重错误。\n" +
" - **示例演示**\n" +
" * 自动存储(职业):用户说“我是网络工程师” -> (捕捉到职业信息) -> **立即自动调用** `add_memory(content='用户职业是网络工程师')` -> (存储成功) -> 回复“原来是同行,网络工程很有趣...”。\n" +
" * 自动查询(场景):用户说“根据我的爱好推荐旅游地点” -> **严禁**直接问“你有什么爱好?” -> **必须立即调用** `query_memory(queryText='用户爱好')` -> (若查到:爬山) -> 回复“既然你喜欢爬山,推荐去黄山...”。\n" +
" * 自动查询(常规):用户问“今天吃什么好?” -> (需要了解口味) -> **立即自动调用** `query_memory(queryText='用户饮食偏好')` -> (获取到不吃香菜) -> 回复“推荐一家不放香菜的...”。\n\n";
/**
* ai写作提示词
*/
public static final String AI_WRITER_PROMPT ="请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。";
/**
* ai写作回复提示词
*/
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
} }

View File

@ -1,17 +1,17 @@
package org.jeecg.modules.airag.app.controller; package org.jeecg.modules.airag.app.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController; import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils; import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.TokenUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig; import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.config.shiro.IgnoreAuth; import org.jeecg.config.satoken.IgnoreAuth;
import org.jeecg.modules.airag.app.consts.AiAppConsts; import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp; import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.service.IAiragAppService; import org.jeecg.modules.airag.app.service.IAiragAppService;
@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
/** /**
* @Description: AI应用 * @Description: AI应用
@ -66,7 +67,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
* @return * @return
*/ */
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:app:edit") @SaCheckPermission("airag:app:edit")
public Result<String> edit(@RequestBody AiragApp airagApp) { public Result<String> edit(@RequestBody AiragApp airagApp) {
AssertUtils.assertNotEmpty("参数异常", airagApp); AssertUtils.assertNotEmpty("参数异常", airagApp);
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName()); AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
@ -105,7 +106,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
* @return * @return
*/ */
@DeleteMapping(value = "/delete") @DeleteMapping(value = "/delete")
@RequiresPermissions("airag:app:delete") @SaCheckPermission("airag:app:delete")
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) { public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------ //update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的 //如果是saas隔离的情况下判断当前租户id是否是当前租户下的
@ -178,16 +179,4 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
return (SseEmitter) airagAppService.generatePrompt(prompt,false); return (SseEmitter) airagAppService.generatePrompt(prompt,false);
} }
/**
* 根据应用ID生成变量和记忆提示词 (SSE)
* for: 【QQYUN-14479】提示词单独拆分
* @param variables
* @return
*/
@PostMapping(value = "/prompt/generateMemoryByAppId")
public SseEmitter generatePromptByAppIdSse(@RequestParam(name = "variables") String variables,
@RequestParam(name = "memoryId") String memoryId) {
return (SseEmitter) airagAppService.generateMemoryByAppId(variables, memoryId,false);
}
} }

View File

@ -6,9 +6,8 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.CommonUtils; import org.jeecg.common.util.CommonUtils;
import org.jeecg.config.shiro.IgnoreAuth; import org.jeecg.config.satoken.IgnoreAuth;
import org.jeecg.modules.airag.app.service.IAiragChatService; import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
import org.jeecg.modules.airag.app.vo.ChatConversation; import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams; import org.jeecg.modules.airag.app.vo.ChatSendParams;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -103,19 +102,6 @@ public class AiragChatController {
return chatService.getConversations(appId); return chatService.getConversations(appId);
} }
/**
* 根据类型获取所有对话
*
* @return 返回一个Result对象包含所有对话的信息
* @author wangshuai
* @date 2025/12/11 11:42
*/
@IgnoreAuth
@GetMapping(value = "/getConversationsByType")
public Result<?> getConversationsByType(@RequestParam(value = "sessionType") String sessionType) {
return chatService.getConversationsByType(sessionType);
}
/** /**
* 删除会话 * 删除会话
* *
@ -127,22 +113,7 @@ public class AiragChatController {
@IgnoreAuth @IgnoreAuth
@DeleteMapping(value = "/conversation/{id}") @DeleteMapping(value = "/conversation/{id}")
public Result<?> deleteConversation(@PathVariable("id") String id) { public Result<?> deleteConversation(@PathVariable("id") String id) {
return chatService.deleteConversation(id,""); return chatService.deleteConversation(id);
}
/**
* 删除会话
*
* @param id
* @return
* @author wangshuai
* @date 2025/12/11 20:00
*/
@IgnoreAuth
@DeleteMapping(value = "/conversation/{id}/{sessionType}")
public Result<?> deleteConversationByType(@PathVariable("id") String id,
@PathVariable("sessionType") String sessionType) {
return chatService.deleteConversation(id,sessionType);
} }
/** /**
@ -168,9 +139,8 @@ public class AiragChatController {
*/ */
@IgnoreAuth @IgnoreAuth
@GetMapping(value = "/messages") @GetMapping(value = "/messages")
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId, public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
@RequestParam(value = "sessionType", required = false) String sessionType) { return chatService.getMessages(conversationId);
return chatService.getMessages(conversationId, sessionType);
} }
/** /**
@ -183,21 +153,7 @@ public class AiragChatController {
@IgnoreAuth @IgnoreAuth
@GetMapping(value = "/messages/clear/{conversationId}") @GetMapping(value = "/messages/clear/{conversationId}")
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) { public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
return chatService.clearMessage(conversationId, ""); return chatService.clearMessage(conversationId);
}
/**
* 清空消息
*
* @return
* @author wangshuai
* @date 2025/12/11 19:06
*/
@IgnoreAuth
@GetMapping(value = "/messages/clear/{conversationId}/{sessionType}")
public Result<?> clearMessageByType(@PathVariable(value = "conversationId") String conversationId,
@PathVariable(value = "sessionType") String sessionType) {
return chatService.clearMessage(conversationId, sessionType);
} }
/** /**
@ -261,25 +217,4 @@ public class AiragChatController {
return result; return result;
} }
/**
* ai海报生成
* @return
*/
@PostMapping("/genAiPoster")
public Result<String> genAiPoster(@RequestBody ChatSendParams chatSendParams){
String imageUrl = chatService.genAiPoster(chatSendParams);
return Result.OK(imageUrl);
}
/**
* 生成ai写作
*
* @param aiWriteGenerateVo
* @return
*/
@PostMapping("/genAiWriter")
public SseEmitter genAiWriter(@RequestBody AiWriteGenerateVo aiWriteGenerateVo){
return chatService.genAiWriter(aiWriteGenerateVo);
}
} }

View File

@ -173,29 +173,6 @@ public class AiragApp implements Serializable {
@Schema(description = "插件") @Schema(description = "插件")
private java.lang.String plugins; private java.lang.String plugins;
/**
* 是否开启记忆(0 不开启1开启)
*/
@Schema(description = "是否开启记忆(0 不开启1开启)")
private java.lang.Integer izOpenMemory;
/**
* 记忆库知识库的id
*/
@Schema(description = "记忆库")
private java.lang.String memoryId;
/**
* 变量
*/
@Schema(description = "变量")
private java.lang.String variables;
/**
* 记忆和变量提示词
*/
@Schema(description = "记忆和变量提示词")
private java.lang.String memoryPrompt;
/** /**
* 知识库ids * 知识库ids
*/ */

View File

@ -1,6 +1,7 @@
package org.jeecg.modules.airag.app.service; package org.jeecg.modules.airag.app.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.app.entity.AiragApp; import org.jeecg.modules.airag.app.entity.AiragApp;
/** /**
@ -20,14 +21,4 @@ public interface IAiragAppService extends IService<AiragApp> {
* @date 2025/3/12 14:45 * @date 2025/3/12 14:45
*/ */
Object generatePrompt(String prompt,boolean blocking); Object generatePrompt(String prompt,boolean blocking);
/**
* 根据应用id生成提示词
*
* @param variables
* @param memoryId
* @param blocking
* @return
*/
Object generateMemoryByAppId(String variables, String memoryId, boolean blocking);
} }

View File

@ -1,7 +1,6 @@
package org.jeecg.modules.airag.app.service; package org.jeecg.modules.airag.app.service;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
import org.jeecg.modules.airag.app.vo.AppDebugParams; import org.jeecg.modules.airag.app.vo.AppDebugParams;
import org.jeecg.modules.airag.app.vo.ChatConversation; import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams; import org.jeecg.modules.airag.app.vo.ChatSendParams;
@ -60,23 +59,21 @@ public interface IAiragChatService {
* 获取对话聊天记录 * 获取对话聊天记录
* *
* @param conversationId * @param conversationId
* @param sessionType 类型
* @return * @return
* @author chenrui * @author chenrui
* @date 2025/2/26 15:16 * @date 2025/2/26 15:16
*/ */
Result<?> getMessages(String conversationId, String sessionType); Result<?> getMessages(String conversationId);
/** /**
* 删除会话 * 删除会话
* *
* @param conversationId * @param conversationId
* @param sessionType
* @return * @return
* @author chenrui * @author chenrui
* @date 2025/3/3 16:55 * @date 2025/3/3 16:55
*/ */
Result<?> deleteConversation(String conversationId, String sessionType); Result<?> deleteConversation(String conversationId);
/** /**
* 更新会话标题 * 更新会话标题
@ -90,12 +87,11 @@ public interface IAiragChatService {
/** /**
* 清空消息 * 清空消息
* @param conversationId * @param conversationId
* @param sessionType
* @return * @return
* @author chenrui * @author chenrui
* @date 2025/3/3 19:49 * @date 2025/3/3 19:49
*/ */
Result<?> clearMessage(String conversationId, String sessionType); Result<?> clearMessage(String conversationId);
/** /**
* 初始化聊天(忽略租户) * 初始化聊天(忽略租户)
@ -115,27 +111,4 @@ public interface IAiragChatService {
* @date 2025/8/11 17:39 * @date 2025/8/11 17:39
*/ */
SseEmitter receiveByRequestId(String requestId); SseEmitter receiveByRequestId(String requestId);
/**
* 根据类型获取会话列表
*
* @param sessionType
* @return
*/
Result<?> getConversationsByType(String sessionType);
/**
* 生成海报图片
* @param chatSendParams
* @return
*/
String genAiPoster(ChatSendParams chatSendParams);
/**
* 生成ai创作
*
* @param chatSendParams
* @return
*/
SseEmitter genAiWriter(AiWriteGenerateVo chatSendParams);
} }

View File

@ -1,44 +0,0 @@
package org.jeecg.modules.airag.app.service;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.common.handler.AIChatParams;
public interface IAiragVariableService {
/**
* 更新变量值
*
* @param userId
* @param appId
* @param name
* @param value
*/
void updateVariable(String userId, String appId, String name, String value);
/**
* 追加提示词
*
* @param username
* @param app
* @return
*/
String additionalPrompt(String username, AiragApp app);
/**
* 初始化变量(仅不存在时设置)
*
* @param userId
* @param appId
* @param name
* @param defaultValue
*/
void initVariable(String userId, String appId, String name, String defaultValue);
/**
* 添加变量更新工具
*
* @param params
* @param aiApp
* @param username
*/
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
}

View File

@ -1,6 +1,5 @@
package org.jeecg.modules.airag.app.service.impl; package org.jeecg.modules.airag.app.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.AiMessage;
@ -11,15 +10,12 @@ import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.service.TokenStream; import dev.langchain4j.service.TokenStream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.util.AssertUtils; import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.UUIDGenerator; import org.jeecg.common.util.UUIDGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.app.consts.Prompts; import org.jeecg.modules.airag.app.consts.Prompts;
import org.jeecg.modules.airag.app.entity.AiragApp; import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper; import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragAppService; import org.jeecg.modules.airag.app.service.IAiragAppService;
import org.jeecg.modules.airag.app.vo.AppVariableVo;
import org.jeecg.modules.airag.common.consts.AiragConsts; import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.handler.AIChatParams; import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler; import org.jeecg.modules.airag.common.handler.IAIChatHandler;
@ -27,8 +23,6 @@ import org.jeecg.modules.airag.common.utils.AiragLocalCache;
import org.jeecg.modules.airag.common.vo.event.EventData; import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventFlowData; import org.jeecg.modules.airag.common.vo.event.EventFlowData;
import org.jeecg.modules.airag.common.vo.event.EventMessageData; import org.jeecg.modules.airag.common.vo.event.EventMessageData;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@ -37,7 +31,6 @@ import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
/** /**
* @Description: AI应用 * @Description: AI应用
@ -52,9 +45,6 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
@Autowired @Autowired
IAIChatHandler aiChatHandler; IAIChatHandler aiChatHandler;
@Autowired
private IAiragKnowledgeService airagKnowledgeService;
@Override @Override
public Object generatePrompt(String prompt, boolean blocking) { public Object generatePrompt(String prompt, boolean blocking) {
AssertUtils.assertNotEmpty("请输入提示词", prompt); AssertUtils.assertNotEmpty("请输入提示词", prompt);
@ -72,167 +62,81 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
} }
return Result.OK("success", promptValue); return Result.OK("success", promptValue);
}else{ }else{
//update-begin---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要--- SseEmitter emitter = new SseEmitter(-0L);
return startSseChat(messages, params); // 异步运行(流式)
//update-end---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要--- TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
} /**
} * 是否正在思考
*/
//update-begin---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆将记忆提示词单独拆分--- AtomicBoolean isThinking = new AtomicBoolean(false);
@Override String requestId = UUIDGenerator.generate();
public Object generateMemoryByAppId(String variables, String memoryId, boolean blocking) { // ai聊天响应逻辑
if(oConvertUtils.isEmpty(variables) && oConvertUtils.isEmpty(memoryId)){ tokenStream.onPartialResponse((String resMessage) -> {
throw new JeecgBootBizTipException("请先添加变量或者记忆后再次重试!"); // 兼容推理模型
} if ("<think>".equals(resMessage)) {
// 构建变量描述 isThinking.set(true);
StringBuilder variablesDesc = new StringBuilder(); resMessage = "> ";
if (oConvertUtils.isNotEmpty(variables)) { }
List<AppVariableVo> variableList = JSONArray.parseArray(variables, AppVariableVo.class); if ("</think>".equals(resMessage)) {
if (variableList != null && !variableList.isEmpty()) { isThinking.set(false);
for (AppVariableVo var : variableList) { resMessage = "\n\n";
if (var.getEnable() != null && !var.getEnable()) { }
continue; if (isThinking.get()) {
} if (null != resMessage && resMessage.contains("\n")) {
String name = var.getName(); resMessage = "\n> ";
if (oConvertUtils.isNotEmpty(var.getAction())) {
String action = var.getAction();
if (oConvertUtils.isNotEmpty(name)) {
try {
// 使用正则替换未被{{}}包裹的变量名
String regex = "(?<!\\{\\{)\\b" + Pattern.quote(name) + "\\b(?!\\}\\})";
action = action.replaceAll(regex, "{{" + name + "}}");
} catch (Exception e) {
log.warn("变量名替换异常: name={}", name, e);
} }
} }
variablesDesc.append(action).append("\n"); EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
} else { EventMessageData messageEventData = EventMessageData.builder()
variablesDesc.append("- {{").append(name).append("}}"); .message(resMessage)
if (oConvertUtils.isNotEmpty(var.getDescription())) { .build();
variablesDesc.append(": ").append(var.getDescription()); eventData.setData(messageEventData);
}
variablesDesc.append("\n");
}
}
}
}
// 构建Prompt
StringBuilder promptBuilder = new StringBuilder(Prompts.GENERATE_GUIDE_HEADER);
if (!variablesDesc.isEmpty()) {
promptBuilder.append(String.format(Prompts.GENERATE_VAR_PART, variablesDesc.toString()));
}
// 构建记忆状态描述
if (oConvertUtils.isNotEmpty(memoryId)) {
String memoryDescr = "";
AiragKnowledge memory = airagKnowledgeService.getById(memoryId);
if (memory != null && oConvertUtils.isNotEmpty(memory.getDescr())) {
memoryDescr += "记忆库描述:" + memory.getDescr();
}
promptBuilder.append(String.format(Prompts.GENERATE_MEMORY_PART, memoryDescr));
}
String prompt = promptBuilder.toString();
List<ChatMessage> messages = List.of(new UserMessage(prompt));
AIChatParams params = new AIChatParams();
params.setTemperature(0.7);
if(blocking){
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
if (promptValue == null || promptValue.isEmpty()) {
return Result.error("生成失败");
}
return Result.OK("success", promptValue);
}else{
return startSseChat(messages, params);
}
}
/**
* 发送聊天
* @param messages
* @param params
* @return
*/
private SseEmitter startSseChat(List<ChatMessage> messages, AIChatParams params) {
SseEmitter emitter = new SseEmitter(-0L);
// 异步运行(流式)
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
/**
* 是否正在思考
*/
AtomicBoolean isThinking = new AtomicBoolean(false);
String requestId = UUIDGenerator.generate();
// ai聊天响应逻辑
tokenStream.onPartialResponse((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
EventMessageData messageEventData = EventMessageData.builder()
.message(resMessage)
.build();
eventData.setData(messageEventData);
try {
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.onCompleteResponse((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
try { try {
log.debug("[AI应用]接收LLM返回消息完成:{}", respText); String eventStr = JSONObject.toJSONString(eventData);
emitter.send(SseEmitter.event().data(eventData)); log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
closeSSE(emitter, eventData); })
} else { .onCompleteResponse((responseMessage) -> {
// 异常结束 // 记录ai的回复
log.error("调用模型异常:" + respText); AiMessage aiMessage = responseMessage.aiMessage();
if (respText.contains("insufficient Balance")) { FinishReason finishReason = responseMessage.finishReason();
respText = "大预言模型账号余额不足!"; String respText = aiMessage.text();
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
try {
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
throw new RuntimeException(e);
}
closeSSE(emitter, eventData);
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
} }
})
.onError((Throwable error) -> {
// sse
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR); EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(respText).build()); eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData); closeSSE(emitter, eventData);
} })
}) .start();
.onError((Throwable error) -> { return emitter;
// sse }
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
})
.start();
return emitter;
} }
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆将记忆提示词单独拆分---
private static void closeSSE(SseEmitter emitter, EventData eventData) { private static void closeSSE(SseEmitter emitter, EventData eventData) {
try { try {

View File

@ -1,36 +1,24 @@
package org.jeecg.modules.airag.app.service.impl; package org.jeecg.modules.airag.app.service.impl;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolExecutionRequest; import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.image.Image; import dev.langchain4j.data.image.Image;
import dev.langchain4j.data.message.*; import dev.langchain4j.data.message.*;
import dev.langchain4j.model.output.FinishReason; import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.service.TokenStream; import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.tool.ToolExecutor;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.tika.parser.AutoDetectParser;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootBizTipException; import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.api.ISysBaseAPI; import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.JwtUtil; import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.*; import org.jeecg.common.util.*;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.vo.Path;
import org.jeecg.modules.airag.app.consts.AiAppConsts; import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.consts.Prompts;
import org.jeecg.modules.airag.app.entity.AiragApp; import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper; import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragChatService; import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.service.IAiragVariableService;
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
import org.jeecg.modules.airag.app.vo.AppDebugParams; import org.jeecg.modules.airag.app.vo.AppDebugParams;
import org.jeecg.modules.airag.app.vo.ChatConversation; import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams; import org.jeecg.modules.airag.app.vo.ChatSendParams;
@ -47,26 +35,18 @@ import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.entity.AiragFlow; import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.service.IAiragFlowService; import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams; import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
import org.jeecg.modules.airag.llm.entity.AiragModel; import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.handler.AIChatHandler; import org.jeecg.modules.airag.llm.handler.AIChatHandler;
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider; import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper; import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations; import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -106,18 +86,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
@Autowired @Autowired
AiragModelMapper airagModelMapper; AiragModelMapper airagModelMapper;
@Autowired
IAiragFlowPluginService airagFlowPluginService;
@Autowired
IAiragKnowledgeService airagKnowledgeService;
@Autowired
IAiragVariableService airagVariableService;
@Autowired
JeecgBaseConfig jeecgBaseConfig;
/** /**
* 重新接收消息 * 重新接收消息
*/ */
@ -137,13 +105,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) { if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId()); app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
} }
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId, chatSendParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
// 更新标题 // 更新标题
if (oConvertUtils.isEmpty(chatConversation.getTitle())) { if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH; chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
chatConversation.setTitle(userMessage.length() > maxLength ? userMessage.substring(0, maxLength) : userMessage);
} }
//update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------ //update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 保存工作流入参配置(如果有) // 保存工作流入参配置(如果有)
@ -151,12 +116,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
chatConversation.setFlowInputs(chatSendParams.getFlowInputs()); chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
} }
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------ //update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
//是否保存会话
if(null != chatSendParams.getIzSaveSession()){
chatConversation.setIzSaveSession(chatSendParams.getIzSaveSession());
}
// 保存变量
saveVariables(app);
// 发送消息 // 发送消息
return doChat(chatConversation, topicId, chatSendParams); return doChat(chatConversation, topicId, chatSendParams);
} }
@ -171,9 +130,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate()); String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
AiragApp app = appDebugParams.getApp(); AiragApp app = appDebugParams.getApp();
app.setId("__DEBUG_APP"); app.setId("__DEBUG_APP");
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId, "");
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
//update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------ //update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 保存工作流入参配置(如果有) // 保存工作流入参配置(如果有)
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) { if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
@ -183,9 +140,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 发送消息 // 发送消息
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams); SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
//保存会话 //保存会话
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- saveChatConversation(chatConversation, true, null);
saveChatConversation(chatConversation, true, null, "");
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
return emitter; return emitter;
} }
@ -292,11 +247,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
} }
@Override @Override
public Result<?> getMessages(String conversationId, String sessionType) { public Result<?> getMessages(String conversationId) {
AssertUtils.assertNotEmpty("请先选择会话", conversationId); AssertUtils.assertNotEmpty("请先选择会话", conversationId);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- String key = getConversationCacheKey(conversationId, null);
String key = getConversationCacheKey(conversationId, null, sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isEmpty(key)) { if (oConvertUtils.isEmpty(key)) {
return Result.ok(Collections.emptyList()); return Result.ok(Collections.emptyList());
} }
@ -320,7 +273,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
.role(msg.getRole()) .role(msg.getRole())
.content(msg.getContent()) .content(msg.getContent())
.images(msg.getImages()) .images(msg.getImages())
.files(msg.getFiles())
.datetime(msg.getDatetime()) .datetime(msg.getDatetime())
.build(); .build();
// 不设置toolExecutionRequests和toolExecutionResult // 不设置toolExecutionRequests和toolExecutionResult
@ -330,30 +282,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
} }
result.put("messages", messages); result.put("messages", messages);
result.put("flowInputs", chatConversation.getFlowInputs()); result.put("flowInputs", chatConversation.getFlowInputs());
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if(oConvertUtils.isNotEmpty(sessionType)){
result.put("appData", chatConversation.getApp());
}
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
return Result.ok(result); return Result.ok(result);
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------ //update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
} }
@Override @Override
public Result<?> clearMessage(String conversationId, String sessionType) { public Result<?> clearMessage(String conversationId) {
AssertUtils.assertNotEmpty("请先选择会话", conversationId); AssertUtils.assertNotEmpty("请先选择会话", conversationId);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- String key = getConversationCacheKey(conversationId, null);
String key = getConversationCacheKey(conversationId, null,sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isEmpty(key)) { if (oConvertUtils.isEmpty(key)) {
return Result.ok(Collections.emptyList()); return Result.ok(Collections.emptyList());
} }
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get(); ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) { if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
chatConversation.getMessages().clear(); chatConversation.getMessages().clear();
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- saveChatConversation(chatConversation);
saveChatConversation(chatConversation,sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
} }
return Result.ok(); return Result.ok();
} }
@ -500,11 +443,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
} }
@Override @Override
public Result<?> deleteConversation(String conversationId, String sessionType) { public Result<?> deleteConversation(String conversationId) {
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId); AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- String key = getConversationCacheKey(conversationId, null);
String key = getConversationCacheKey(conversationId, null, sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isNotEmpty(key)) { if (oConvertUtils.isNotEmpty(key)) {
Boolean delete = redisTemplate.delete(key); Boolean delete = redisTemplate.delete(key);
if (delete) { if (delete) {
@ -522,18 +463,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams); AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId()); AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle()); AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
String key = getConversationCacheKey(updateTitleParams.getId(), null, updateTitleParams.getSessionType()); String key = getConversationCacheKey(updateTitleParams.getId(), null);
if (oConvertUtils.isEmpty(key)) { if (oConvertUtils.isEmpty(key)) {
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId()); log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
return Result.ok(); return Result.ok();
} }
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get(); ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- chatConversation.setTitle(updateTitleParams.getTitle());
if (chatConversation != null) { saveChatConversation(chatConversation);
chatConversation.setTitle(updateTitleParams.getTitle());
}
saveChatConversation(chatConversation,updateTitleParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
return Result.ok(); return Result.ok();
} }
@ -542,21 +479,15 @@ public class AiragChatServiceImpl implements IAiragChatService {
* *
* @param conversationId * @param conversationId
* @param httpRequest * @param httpRequest
* @param sessionType 会话类型
* @return * @return
* @author chenrui * @author chenrui
* @date 2025/2/25 19:27 * @date 2025/2/25 19:27
*/ */
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest, String sessionType) { private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
if (oConvertUtils.isEmpty(conversationId)) { if (oConvertUtils.isEmpty(conversationId)) {
return null; return null;
} }
String key = getConversationDirCacheKey(httpRequest); String key = getConversationDirCacheKey(httpRequest);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if(oConvertUtils.isNotEmpty(sessionType)){
key = key + ":" + sessionType;
}
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
key = key + ":" + conversationId; key = key + ":" + conversationId;
return key; return key;
} }
@ -591,21 +522,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
* *
* @param app * @param app
* @param conversationId * @param conversationId
* @param sessionType
* @return * @return
* @author chenrui * @author chenrui
* @date 2025/2/25 19:19 * @date 2025/2/25 19:19
*/ */
@NotNull @NotNull
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId, String sessionType) { private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId) {
if (oConvertUtils.isObjectEmpty(app)) { if (oConvertUtils.isObjectEmpty(app)) {
app = new AiragApp(); app = new AiragApp();
app.setId(AiAppConsts.DEFAULT_APP_ID); app.setId(AiAppConsts.DEFAULT_APP_ID);
} }
ChatConversation chatConversation = null; ChatConversation chatConversation = null;
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- String key = getConversationCacheKey(conversationId, null);
String key = getConversationCacheKey(conversationId, null,sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isNotEmpty(key)) { if (oConvertUtils.isNotEmpty(key)) {
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get(); chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
} }
@ -641,8 +569,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui * @author chenrui
* @date 2025/2/25 19:27 * @date 2025/2/25 19:27
*/ */
private void saveChatConversation(ChatConversation chatConversation, String sessionType) { private void saveChatConversation(ChatConversation chatConversation) {
saveChatConversation(chatConversation, false, null, sessionType); saveChatConversation(chatConversation, false, null);
} }
/** /**
@ -653,19 +581,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui * @author chenrui
* @date 2025/2/25 19:27 * @date 2025/2/25 19:27
*/ */
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest, String sessionType) { private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
if (null == chatConversation) { if (null == chatConversation) {
return; return;
} }
String key = getConversationCacheKey(chatConversation.getId(), httpRequest);
//如果是不保存会话直接返回
if(null != chatConversation.getIzSaveSession() && !chatConversation.getIzSaveSession()){
return;
}
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
String key = getConversationCacheKey(chatConversation.getId(), httpRequest, sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isEmpty(key)) { if (oConvertUtils.isEmpty(key)) {
return; return;
} }
@ -760,10 +680,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @date 2025/2/25 19:05 * @date 2025/2/25 19:05
*/ */
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) { private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) {
appendMessage(messages, message, chatConversation, topicId, null, null);
}
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId, List<String> files, String saveContent) {
if (message.type().equals(ChatMessageType.SYSTEM)) { if (message.type().equals(ChatMessageType.SYSTEM)) {
// 系统消息,放到消息列表最前面,并且不记录历史 // 系统消息,放到消息列表最前面,并且不记录历史
@ -793,22 +709,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
textContent.append(((TextContent) content).text()).append("\n"); textContent.append(((TextContent) content).text()).append("\n");
} }
}); });
//update-begin---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档--- historyMessage.setContent(textContent.toString());
if (oConvertUtils.isNotEmpty(saveContent)) {
historyMessage.setContent(saveContent);
} else {
historyMessage.setContent(textContent.toString());
}
historyMessage.setImages(images); historyMessage.setImages(images);
// 保存文件信息
if (oConvertUtils.isNotEmpty(files)) {
List<MessageHistory.FileHistory> fileHistories = new ArrayList<>();
for (String file : files) {
fileHistories.add(new MessageHistory.FileHistory(file));
}
historyMessage.setFiles(fileHistories);
}
//update-end---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档---
} else if (message.type().equals(ChatMessageType.AI)) { } else if (message.type().equals(ChatMessageType.AI)) {
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI); historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
AiMessage aiMessage = (AiMessage) message; AiMessage aiMessage = (AiMessage) message;
@ -864,20 +766,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>()); AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
try { try {
// 组装用户消息 // 组装用户消息
String content = sendParams.getContent(); UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
//将文件内容给提示词
if(!CollectionUtils.isEmpty(sendParams.getFiles())){
content = buildContentWithFiles(content, sendParams.getFiles());
}
UserMessage userMessage = aiChatHandler.buildUserMessage(content, sendParams.getImages());
// 追加消息 // 追加消息
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档--- appendMessage(messages, userMessage, chatConversation, topicId);
appendMessage(messages, userMessage, chatConversation, topicId, sendParams.getFiles(), sendParams.getContent());
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档---
// 绘画AI逻辑当开启生成绘画时调用
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableDraw()) && sendParams.getEnableDraw()) {
return genImageChat(emitter,sendParams,requestId,messages,chatConversation,topicId);
}
/* 这里应该是有几种情况: /* 这里应该是有几种情况:
* 1. 非ai应用:获取默认模型->开始聊天 * 1. 非ai应用:获取默认模型->开始聊天
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天 * 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
@ -890,7 +781,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams); sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
} else { } else {
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词 // AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams, aiApp.getFlowId(), aiApp.getMemoryId()); sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
} }
} else { } else {
// 发消息 // 发消息
@ -898,13 +789,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) { if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
aiChatParams.setEnableSearch(sendParams.getEnableSearch()); aiChatParams.setEnableSearch(sendParams.getEnableSearch());
} }
// 设置深度思考搜索参数 sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams);
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
aiChatParams.setReturnThinking(sendParams.getEnableThink());
}
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams, sendParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
} }
// 发送就绪消息 // 发送就绪消息
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId); EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
@ -919,59 +804,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
return emitter; return emitter;
} }
/**
* 生成图片
*
* @param emitter
* @param sendParams
* @param requestId
* @param messages
* @param chatConversation
* @param topicId
* @return
*/
private SseEmitter genImageChat(SseEmitter emitter, ChatSendParams sendParams, String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
AssertUtils.assertNotEmpty("请选择绘画模型", sendParams.getDrawModelId());
AIChatParams aiChatParams = new AIChatParams();
try {
List<String> images = sendParams.getImages();
List<Map<String, Object>> imageList = new ArrayList<>();
if(CollectionUtils.isEmpty(images)) {
//生成图片
imageList = aiChatHandler.imageGenerate(sendParams.getDrawModelId(), sendParams.getContent(), aiChatParams);
} else {
//图生图
imageList = aiChatHandler.imageEdit(sendParams.getDrawModelId(), sendParams.getContent(), images, aiChatParams);
}
// 记录历史消息
String imageMarkdown = imageList.stream().map(map -> {
String newUrl = this.uploadImage(map);
return "![](" + newUrl + ")";
}).collect(Collectors.joining("\n"));
AiMessage aiMessage = new AiMessage(imageMarkdown);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 处理绘画结果并通过SSE返回给客户端
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message(imageMarkdown).build();
eventData.setData(messageEventData);
eventData.setRequestId(requestId);
sendMessage2Client(emitter, eventData);
// 保存会话
saveChatConversation(chatConversation, false, SpringContextUtils.getHttpServletRequest(), sendParams.getSessionType());
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
eventData.setRequestId(requestId);
sendMessage2Client(emitter, eventData);
} catch (Exception e) {
log.error("绘画AI调用异常", e);
EventData errorEventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message("绘画AI调用失败" + e.getMessage()).build();
errorEventData.setData(messageEventData);
errorEventData.setRequestId(requestId);
closeSSE(emitter, errorEventData);
}
return emitter;
}
/** /**
* 运行流程 * 运行流程
* *
@ -1043,9 +875,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
sendMessage2Client(emitter, msgEventData); sendMessage2Client(emitter, msgEventData);
appendMessage(messages, aiMessage, chatConversation, topicId); appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话 // 保存会话
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- saveChatConversation(chatConversation, false, httpRequest);
saveChatConversation(chatConversation, false, httpRequest, sendParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
} }
}else{ }else{
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------ //update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
@ -1078,31 +908,16 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @param chatConversation * @param chatConversation
* @param topicId * @param topicId
* @param sendParams * @param sendParams
* @param flowId
* @param memoryId
* @return * @return
* @author chenrui * @author chenrui
* @date 2025/2/28 10:41 * @date 2025/2/28 10:41
*/ */
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams, String flowId, String memoryId) { private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
AiragApp aiApp = chatConversation.getApp(); AiragApp aiApp = chatConversation.getApp();
String modelId = aiApp.getModelId(); String modelId = aiApp.getModelId();
AssertUtils.assertNotEmpty("请先选择模型", modelId); AssertUtils.assertNotEmpty("请先选择模型", modelId);
// AI应用提示词 // AI应用提示词
String prompt = aiApp.getPrompt(); String prompt = aiApp.getPrompt();
String username = "jeecg";
try {
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
username = JwtUtil.getUserNameByToken(req);
} catch (Exception e) {
log.error(e.getMessage());
}
//将变量中的题试题替换并追加
if(oConvertUtils.isObjectNotEmpty(aiApp.getVariables())) {
prompt = airagVariableService.additionalPrompt(username, aiApp);
}
if (oConvertUtils.isNotEmpty(prompt)) { if (oConvertUtils.isNotEmpty(prompt)) {
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId); appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
} }
@ -1128,9 +943,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (metadata.containsKey("maxTokens")) { if (metadata.containsKey("maxTokens")) {
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens")); aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
} }
if (metadata.containsKey(FlowConsts.FLOW_NODE_OPTION_TIME_OUT)) {
aiChatParams.setTimeout(oConvertUtils.getInt(metadata.getInteger(FlowConsts.FLOW_NODE_OPTION_TIME_OUT), 300));
}
} }
} }
@ -1153,69 +965,15 @@ public class AiragChatServiceImpl implements IAiragChatService {
} }
} }
//流程不为空,构建插件
if(oConvertUtils.isNotEmpty(flowId)){
Map<String, Object> result = airagFlowPluginService.getFlowsToPlugin(flowId);
this.addPluginToParams(aiChatParams, result);
}
// 设置网络搜索参数(如果前端传递了) // 设置网络搜索参数(如果前端传递了)
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) { if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
aiChatParams.setEnableSearch(sendParams.getEnableSearch()); aiChatParams.setEnableSearch(sendParams.getEnableSearch());
} }
// 设置深度思考参数(如果前端传递了)
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
aiChatParams.setReturnThinking(sendParams.getEnableThink());
}
// 设置记忆库的插件
if(sendParams != null && oConvertUtils.isNotEmpty(memoryId)){
//开启记忆
if(null == aiApp.getIzOpenMemory() || AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())){
Map<String, Object> pluginMemory = airagKnowledgeService.getPluginMemory(memoryId);
this.addPluginToParams(aiChatParams, pluginMemory);
}
}
//设置变量的插件
// 添加系统级工具:变量更新
if (oConvertUtils.isNotEmpty(aiApp.getId())) {
airagVariableService.addUpdateVariableTool(aiApp,username,aiChatParams);
}
// 打印流程耗时日志 // 打印流程耗时日志
printChatDuration(requestId, "构造应用自定义参数完成"); printChatDuration(requestId, "构造应用自定义参数完成");
// 发消息 // 发消息
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams, sendParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
}
/**
* 添加插件到参数中
*
* @param aiChatParams
* @param result
*/
private void addPluginToParams(AIChatParams aiChatParams, Map<String, Object> result) {
if (result == null) {
return;
}
Map<ToolSpecification, ToolExecutor> flowsToPlugin = (Map<ToolSpecification, ToolExecutor>) result.get("pluginTool");
String pluginId = (String) result.get("pluginId");
if (aiChatParams.getTools() == null) {
aiChatParams.setTools(new HashMap<>());
}
if (flowsToPlugin != null) {
aiChatParams.getTools().putAll(flowsToPlugin);
}
if (aiChatParams.getPluginIds() == null) {
aiChatParams.setPluginIds(new ArrayList<>());
}
if (oConvertUtils.isNotEmpty(pluginId)) {
aiChatParams.getPluginIds().add(pluginId);
}
} }
/** /**
@ -1226,12 +984,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @param topicId * @param topicId
* @param modelId * @param modelId
* @param messages * @param messages
* @param sessionType
* @return * @return
* @author chenrui * @author chenrui
* @date 2025/2/25 19:24 * @date 2025/2/25 19:24
*/ */
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams, String sessionType) { private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
// 调用ai聊天 // 调用ai聊天
if (null == aiChatParams) { if (null == aiChatParams) {
aiChatParams = new AIChatParams(); aiChatParams = new AIChatParams();
@ -1240,16 +997,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){ if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools()); aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
} }
if(CollectionUtils.isEmpty(aiChatParams.getKnowIds())){ aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
} else {
aiChatParams.getKnowIds().addAll(chatConversation.getApp().getKnowIds());
}
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5)); aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest()); aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
aiChatParams.setReturnThinking(true);
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest(); HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
// for [QQYUN-9234] MCP服务连接关闭 - 保存参数引用用于在回调中关闭MCP连接
final AIChatParams finalAiChatParams = aiChatParams;
TokenStream chatStream; TokenStream chatStream;
try { try {
// 打印流程耗时日志 // 打印流程耗时日志
@ -1261,8 +1013,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
} }
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
// for [QQYUN-9234] MCP服务连接关闭 - 异常时关闭MCP连接
finalAiChatParams.closeMcpConnections();
// sse // sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId); SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) { if (null == emitter) {
@ -1348,8 +1098,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 打印流程耗时日志 // 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息完成"); printChatDuration(requestId, "LLM输出消息完成");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId); AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// for [QQYUN-9234] MCP服务连接关闭 - 聊天完成时关闭MCP连接
finalAiChatParams.closeMcpConnections();
// 记录ai的回复 // 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage(); AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason(); FinishReason finishReason = responseMessage.finishReason();
@ -1365,9 +1113,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId); EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
appendMessage(messages, aiMessage, chatConversation, topicId); appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话 // 保存会话
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- saveChatConversation(chatConversation, false, httpRequest);
saveChatConversation(chatConversation, false, httpRequest, sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
closeSSE(emitter, eventData); closeSSE(emitter, eventData);
} else if (FinishReason.LENGTH.equals(finishReason)) { } else if (FinishReason.LENGTH.equals(finishReason)) {
// 上下文长度超过限制 // 上下文长度超过限制
@ -1391,8 +1137,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 打印流程耗时日志 // 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息异常"); printChatDuration(requestId, "LLM输出消息异常");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId); AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// for [QQYUN-9234] MCP服务连接关闭 - 聊天异常时关闭MCP连接
finalAiChatParams.closeMcpConnections();
// sse // sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId); SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) { if (null == emitter) {
@ -1457,7 +1201,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
*/ */
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) { private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
try { try {
log.debug("发送消息:{}", eventData.getRequestId()); log.info("发送消息:{}", eventData.getRequestId());
String eventStr = JSONObject.toJSONString(eventData); String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr); log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr)); emitter.send(SseEmitter.event().data(eventStr));
@ -1507,9 +1251,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isEmpty(chatConversation.getId())) { if (oConvertUtils.isEmpty(chatConversation.getId())) {
return; return;
} }
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- String key = getConversationCacheKey(chatConversation.getId(), null);
String key = getConversationCacheKey(chatConversation.getId(), null,"");
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isEmpty(key)) { if (oConvertUtils.isEmpty(key)) {
return; return;
} }
@ -1539,13 +1281,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isNotEmpty(summaryTitle)) { if (oConvertUtils.isNotEmpty(summaryTitle)) {
cachedConversation.setTitle(summaryTitle); cachedConversation.setTitle(summaryTitle);
} else { } else {
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH; cachedConversation.setTitle(question.length() > 5 ? question.substring(0, 5) : question);
cachedConversation.setTitle(question.length() > maxLength ? question.substring(0, maxLength) : question);
} }
//保存会话 //保存会话
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户--- saveChatConversation(cachedConversation);
saveChatConversation(cachedConversation,"");
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
} }
}); });
} }
@ -1567,7 +1306,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
} else { } else {
token = TokenUtils.getTokenByRequest(); token = TokenUtils.getTokenByRequest();
} }
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) { if (TokenUtils.verifyToken(token, sysBaseApi)) {
return JwtUtil.getUsername(token); return JwtUtil.getUsername(token);
} }
} catch (Exception e) { } catch (Exception e) {
@ -1590,296 +1329,4 @@ public class AiragChatServiceImpl implements IAiragChatService {
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000); log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
} }
} }
/**
* 根据会话类型获取会话信息
*
* @param sessionType
* @return
*/
@Override
public Result<?> getConversationsByType(String sessionType) {
String key = getConversationDirCacheKey(null);
key = key + ":" + sessionType + ":*";
List<String> keys = redisUtil.scan(key);
// 如果键集合为空,返回空列表
if (keys.isEmpty()) {
return Result.ok(Collections.emptyList());
}
// 遍历键集合,获取对应的 ChatConversation 对象
List<ChatConversation> conversations = new ArrayList<>();
for (Object k : keys) {
ChatConversation conversation = (ChatConversation) redisTemplate.boundValueOps(k).get();
if (conversation != null) {
AiragApp app = conversation.getApp();
if (null == app) {
continue;
}
conversation.setApp(null);
conversation.setMessages(null);
conversations.add(conversation);
}
}
// 对会话列表按创建时间降序排序
conversations.sort((o1, o2) -> {
Date date1 = o1.getCreateTime();
Date date2 = o2.getCreateTime();
if (date1 == null && date2 == null) {
return 0;
}
if (date1 == null) {
return 1;
}
if (date2 == null) {
return -1;
}
return date2.compareTo(date1);
});
// 返回结果
return Result.ok(conversations);
}
//================================================= begin 【QQYUN-14269】【AI】支持变量 ========================================
/**
* 初始化变量(仅不存在时设置)
*/
private void saveVariables(AiragApp app) {
if(null == app){
return;
}
if(!AiAppConsts.IZ_OPEN_MEMORY.equals(app.getIzOpenMemory())){
return;
}
if (oConvertUtils.isObjectNotEmpty(app.getVariables())) {
// 变量替换
String username = "jeecg";
try {
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
username = JwtUtil.getUserNameByToken(req);
} catch (Exception e) {
log.error(e.getMessage());
}
if (oConvertUtils.isNotEmpty(username) && oConvertUtils.isNotEmpty(app.getId())) {
String variables = app.getVariables();
JSONArray objects = JSONArray.parseArray(variables);
for (int i = 0; i < objects.size(); i++) {
JSONObject jsonObject = objects.getJSONObject(i);
String name = jsonObject.getString("name");
String defaultValue = jsonObject.getString("defaultValue");
if (oConvertUtils.isNotEmpty(name)) {
airagVariableService.initVariable(username, app.getId(), name, defaultValue);
}
}
}
}
}
//================================================= end 【QQYUN-14269】【AI】支持变量 ========================================
/**
* ai海报生成
*
* @param chatSendParams
* @return
*/
@Override
public String genAiPoster(ChatSendParams chatSendParams) {
AssertUtils.assertNotEmpty("请选择绘画模型", chatSendParams.getDrawModelId());
AssertUtils.assertNotEmpty("请填写提示词", chatSendParams.getContent());
AIChatParams aiChatParams = new AIChatParams();
if(oConvertUtils.isNotEmpty(chatSendParams.getImageSize())){
aiChatParams.setImageSize(chatSendParams.getImageSize());
}
String image= chatSendParams.getImageUrl();
List<Map<String, Object>> imageList = new ArrayList<>();
if(oConvertUtils.isEmpty(image)) {
//生成图片
imageList = aiChatHandler.imageGenerate(chatSendParams.getDrawModelId(), chatSendParams.getContent(), aiChatParams);
} else {
//图生图
imageList = aiChatHandler.imageEdit(chatSendParams.getDrawModelId(), chatSendParams.getContent(), Arrays.asList(image.split(SymbolConstant.COMMA)), aiChatParams);
}
return imageList.stream().map(this::uploadImage).collect(Collectors.joining("\n"));
}
/**
* 上传图片
*
* @param map
* @return
*/
private String uploadImage(Map<String, Object> map) {
if (null == map || map.isEmpty()) {
return "";
}
try {
String type = String.valueOf(map.get("type"));
String value = String.valueOf(map.get("value"));
byte[] data = new byte[1024];
// 判断是否是base64
if ("base64".equals(type)) {
if(value.startsWith("data:image")){
value = value.substring(value.indexOf(",") + 1);
}
data = Base64.getDecoder().decode(value);
} else {
//下载网络图片
InputStream inputStream = FileDownloadUtils.getDownInputStream(value, "");
if (inputStream != null) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] inpByte = new byte[1024]; // 1KB缓冲区
int nRead;
while ((nRead = inputStream.read(inpByte, 0, data.length)) != -1) {
buffer.write(inpByte, 0, nRead);
}
data = buffer.toByteArray();
}
}
if (data != null) {
Path path = jeecgBaseConfig.getPath();
String bizPath = "chat";
String url = CommonUtils.uploadOnlineImage(data, path.getUpload(), bizPath, jeecgBaseConfig.getUploadType());
if("local".equals(jeecgBaseConfig.getUploadType())){
url = "#{domainURL}/" + url;
}
return url;
}
} catch (Exception e) {
log.error("上传图片失败", e);
}
return "";
}
//================================================= begin【QQYUN-14261】【AI】AI助手支持多模态能力- 文档========================================
/**
* 构建文件内容
*
* @param content
* @param files
* @return
*/
private String buildContentWithFiles(String content, List<String> files) {
String filesText = parseFilesToText(files);
if (oConvertUtils.isEmpty(content)) {
content = "请基于我提供的附件内容回答问题。";
}else{
content = content + "\n\n请基于我提供的附件内容回答问题。";
}
if (oConvertUtils.isNotEmpty(filesText)) {
if (oConvertUtils.isNotEmpty(content)) {
content = content + "\n\n" + filesText;
} else {
content = filesText;
}
}
return content;
}
/**
* 将文件转换成text
*
* @param files
* @return
*/
private String parseFilesToText(List<String> files) {
if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(files)) {
return "";
}
StringBuilder sb = new StringBuilder();
TikaDocumentParser parser = new TikaDocumentParser(AutoDetectParser::new, null, null, null);
int parsedCount = 0;
for (String fileRef : files) {
if (parsedCount >= LLMConsts.CHAT_FILE_MAX_COUNT) {
break;
}
if (oConvertUtils.isEmpty(fileRef)) {
continue;
}
String fileRefWithoutQuery = fileRef;
if (fileRefWithoutQuery.contains("?")) {
fileRefWithoutQuery = fileRefWithoutQuery.substring(0, fileRefWithoutQuery.indexOf("?"));
}
String fileName = FilenameUtils.getName(fileRefWithoutQuery);
String ext = FilenameUtils.getExtension(fileName);
if (oConvertUtils.isEmpty(ext) || !LLMConsts.CHAT_FILE_EXT_WHITELIST.contains(ext.toLowerCase())) {
continue;
}
try {
File file = ensureLocalFile(fileRef, fileName);
if (file == null || !file.exists() || !file.isFile()) {
continue;
}
Document document = parser.parse(file);
if (document == null || oConvertUtils.isEmpty(document.text())) {
continue;
}
String text = document.text().trim();
if (text.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
text = text.substring(0, LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH);
}
sb.append("附件[").append(fileName).append("]内容:\n").append(text).append("\n\n");
parsedCount++;
if (sb.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
break;
}
} catch (Exception e) {
log.warn("附件解析失败: {}, {}", fileRef, e.getMessage());
}
}
return sb.toString().trim();
}
/**
* 获取本地文件
*
* @param fileRef
* @param fileName
* @return
* @throws IOException
*/
private File ensureLocalFile(String fileRef, String fileName) {
String uploadpath = jeecgBaseConfig.getPath().getUpload();
if (LLMConsts.WEB_PATTERN.matcher(fileRef).matches()) {
String tempDir = uploadpath + File.separator + "chat" + File.separator + UUID.randomUUID() + File.separator;
File dir = new File(tempDir);
if (!dir.exists() && !dir.mkdirs()) {
return null;
}
String tempFilePath = tempDir + fileName;
FileDownloadUtils.download2DiskFromNet(fileRef, tempFilePath);
return new File(tempFilePath);
}
return new File(uploadpath + File.separator + fileRef);
}
//================================================= end【QQYUN-14261】【AI】AI助手支持多模态能力- 文档========================================
/**
* ai创作
*
* @param aiWriteGenerateVo
* @return
*/
@Override
public SseEmitter genAiWriter(AiWriteGenerateVo aiWriteGenerateVo) {
String activeMode = "compose";
ChatSendParams sendParams = new ChatSendParams();
sendParams.setAppId(AiAppConsts.WRITER_APP_ID);
String content = "";
//写作
if (activeMode.equals(aiWriteGenerateVo.getActiveMode())) {
content = StrUtil.format(Prompts.AI_WRITER_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
} else {
//回复
content = StrUtil.format(Prompts.AI_REPLY_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getOriginalContent(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
}
sendParams.setContent(content);
sendParams.setIzSaveSession(false);
return this.send(sendParams);
}
} }

View File

@ -1,194 +0,0 @@
package org.jeecg.modules.airag.app.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
import dev.langchain4j.service.tool.ToolExecutor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.service.IAiragVariableService;
import org.jeecg.modules.airag.app.vo.AppVariableVo;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: AI应用变量服务实现
* @Author: jeecg-boot
* @Date: 2025-02-26
* @Version: V1.0
*/
@Service
@Slf4j
public class AiragVariableServiceImpl implements IAiragVariableService {
@Autowired
private RedisTemplate redisTemplate;
private static final String CACHE_PREFIX = "airag:app:var:";
/**
* 初始化变量(仅不存在时设置)
*
* @param username
* @param appId
* @param name
* @param defaultValue
*/
@Override
public void initVariable(String username, String appId, String name, String defaultValue) {
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
return;
}
String key = CACHE_PREFIX + appId + ":" + username;
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
}
/**
* 追加提示词
*
* @param username
* @param app
* @return
*/
@Override
public String additionalPrompt(String username, AiragApp app) {
String memoryPrompt = app.getMemoryPrompt();
String prompt = app.getPrompt();
if (oConvertUtils.isEmpty(memoryPrompt)) {
return prompt;
}
String variablesStr = app.getVariables();
if (oConvertUtils.isEmpty(variablesStr)) {
return prompt;
}
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
if (variableList == null || variableList.isEmpty()) {
return prompt;
}
String key = CACHE_PREFIX + app.getId() + ":" + username;
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
for (AppVariableVo variable : variableList) {
if (variable.getEnable() != null && !variable.getEnable()) {
continue;
}
String name = variable.getName();
String value = variable.getDefaultValue();
// 优先使用Redis中的值
if (savedValues.containsKey(name)) {
Object savedVal = savedValues.get(name);
if (savedVal != null) {
value = String.valueOf(savedVal);
}
}
if (value == null) {
value = "";
}
// 替换 {{name}}
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
}
return prompt + "\n" + memoryPrompt;
}
/**
* 更新变量值
*
* @param userId
* @param appId
* @param name
* @param value
*/
@Override
public void updateVariable(String userId, String appId, String name, String value) {
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
return;
}
String key = CACHE_PREFIX + appId + ":" + userId;
redisTemplate.opsForHash().put(key, name, value);
}
/**
* 添加变量更新工具
*
* @param params
* @param aiApp
* @param username
*/
@Override
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
if (params.getTools() == null) {
params.setTools(new HashMap<>());
}
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
return;
}
// 构建变量描述信息
String variablesStr = aiApp.getVariables();
List<AppVariableVo> variableList = null;
if (oConvertUtils.isNotEmpty(variablesStr)) {
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
}
//工具描述
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
if (variableList != null && !variableList.isEmpty()) {
descriptionBuilder.append("\n\n可用变量列表");
for (AppVariableVo var : variableList) {
if (var.getEnable() != null && !var.getEnable()) {
continue;
}
descriptionBuilder.append("\n- ").append(var.getName());
if (oConvertUtils.isNotEmpty(var.getDescription())) {
descriptionBuilder.append(": ").append(var.getDescription());
}
}
descriptionBuilder.append("\n\n注意variableName必须是上述列表中的名称之一。");
}
//构建更新变量的工具
ToolSpecification spec = ToolSpecification.builder()
.name("update_variable")
.description(descriptionBuilder.toString())
.parameters(JsonObjectSchema.builder()
.addStringProperty("variableName", "变量名称")
.addStringProperty("value", "变量值")
.required("variableName", "value")
.build())
.build();
//监听工具的调用
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
try {
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
String name = args.getString("variableName");
String value = args.getString("value");
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
//更新变量值
variableService.updateVariable(username, aiApp.getId(), name, value);
return "变量 " + name + " 已更新为: " + value;
} catch (Exception e) {
log.error("更新变量失败", e);
return "更新变量失败: " + e.getMessage();
}
};
params.getTools().put(spec, executor);
}
}

View File

@ -1,48 +0,0 @@
package org.jeecg.modules.airag.app.vo;
import lombok.Data;
/**
* @Description: ai写作生成实体类
*
* @author: wangshuai
* @date: 2026/1/12 15:59
*/
@Data
public class AiWriteGenerateVo {
/**
* 写作类型
*/
private String activeMode;
/**
* 写作内容提示
*/
private String prompt;
/**
* 原文
*/
private String originalContent;
/**
* 长度
*/
private String length;
/**
* 格式
*/
private String format;
/**
* 语气
*/
private String tone;
/**
* 语言
*/
private String language;
}

View File

@ -1,46 +0,0 @@
package org.jeecg.modules.airag.app.vo;
import lombok.Data;
import java.io.Serializable;
/**
* @Description: 应用变量配置
* @Author: jeecg-boot
* @Date: 2025-02-26
* @Version: V1.0
*/
@Data
public class AppVariableVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 变量名
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 默认值
*/
private String defaultValue;
/**
* 是否启用
*/
private Boolean enable;
/**
* 动作
*/
private String action;
/**
* 排序
*/
private Integer orderNum;
}

View File

@ -47,14 +47,4 @@ public class ChatConversation {
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程 * for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/ */
private Map<String, Object> flowInputs; private Map<String, Object> flowInputs;
/**
* portal 应用门户
*/
private String sessionType;
/**
* 是否保存会话
*/
private Boolean izSaveSession;
} }

View File

@ -47,11 +47,6 @@ public class ChatSendParams {
*/ */
private List<String> images; private List<String> images;
/**
* 文件列表
*/
private List<String> files;
/** /**
* 工作流额外入参配置 * 工作流额外入参配置
* key: 参数field, value: 参数值 * key: 参数field, value: 参数值
@ -64,39 +59,4 @@ public class ChatSendParams {
*/ */
private Boolean enableSearch; private Boolean enableSearch;
/**
* 是否开启深度思考
*/
private Boolean enableThink;
/**
* 会话类型: portal 应用门户
*/
private String sessionType;
/**
* 是否开启生成绘画
*/
private Boolean enableDraw;
/**
* 绘画模型的id
*/
private String drawModelId;
/**
* 图片尺寸
*/
private String imageSize;
/**
* 一张图片
*/
private String imageUrl;
/**
* 是否保存会话
*/
private Boolean izSaveSession;
} }

View File

@ -1,6 +1,5 @@
package org.jeecg.modules.airag.demo; package org.jeecg.modules.airag.demo;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava; import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -12,15 +11,12 @@ import java.util.Map;
* @Author: chenrui * @Author: chenrui
* @Date: 2025/3/6 11:42 * @Date: 2025/3/6 11:42
*/ */
@Slf4j
@Component("testAiragEnhance") @Component("testAiragEnhance")
public class TestAiragEnhance implements IAiRagEnhanceJava { public class TestAiragEnhance implements IAiRagEnhanceJava {
@Override @Override
public Map<String, Object> process(Map<String, Object> inputParams) { public Map<String, Object> process(Map<String, Object> inputParams) {
Object arg1 = inputParams.get("arg1"); Object arg1 = inputParams.get("arg1");
Object arg2 = inputParams.get("arg2"); Object arg2 = inputParams.get("arg2");
Object index = inputParams.get("index");
log.info("arg1={}, arg2={}, index={}", arg1, arg2, index);
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString()); return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
} }
} }

View File

@ -1,209 +0,0 @@
package org.jeecg.modules.airag.llm.consts;
/**
* @Description: 流程插件常量
*
* @author: wangshuai
* @date: 2025/12/23 19:37
*/
public interface FlowPluginContent {
/**
* 名称
*/
String NAME = "name";
/**
* 描述
*/
String DESCRIPTION = "description";
/**
* 响应
*/
String RESPONSES = "responses";
/**
* 类型
*/
String TYPE = "type";
/**
* 参数
*/
String PARAMETERS = "parameters";
/**
* 是否必须
*/
String REQUIRED = "required";
/**
* 默认值
*/
String DEFAULT_VALUE = "defaultValue";
/**
* 路径
*/
String PATH = "path";
/**
* 方法
*/
String METHOD = "method";
/**
* 位置
*/
String LOCATION = "location";
/**
* 认证类型
*/
String AUTH_TYPE = "authType";
/**
* token参数名称
*/
String TOKEN_PARAM_NAME = "tokenParamName";
/**
* token参数值
*/
String TOKEN_PARAM_VALUE = "tokenParamValue";
/**
* token
*/
String TOKEN = "token";
/**
* Path位置
*/
String LOCATION_PATH = "Path";
/**
* Header位置
*/
String LOCATION_HEADER = "Header";
/**
* Query位置
*/
String LOCATION_QUERY = "Query";
/**
* Body位置
*/
String LOCATION_BODY = "Body";
/**
* Form-Data位置
*/
String LOCATION_FORM_DATA = "Form-Data";
/**
* String类型
*/
String TYPE_STRING = "String";
/**
* string类型
*/
String TYPE_STRING_LOWER = "string";
/**
* Number类型
*/
String TYPE_NUMBER = "Number";
/**
* number类型
*/
String TYPE_NUMBER_LOWER = "number";
/**
* Integer类型
*/
String TYPE_INTEGER = "Integer";
/**
* integer类型
*/
String TYPE_INTEGER_LOWER = "integer";
/**
* Boolean类型
*/
String TYPE_BOOLEAN = "Boolean";
/**
* boolean类型
*/
String TYPE_BOOLEAN_LOWER = "boolean";
/**
* 工具数量
*/
String TOOL_COUNT = "tool_count";
/**
* 是否启用
*/
String ENABLED = "enabled";
/**
* 输入
*/
String INPUTS = "inputs";
/**
* 输出
*/
String OUTPUTS = "outputs";
/**
* POST请求
*/
String POST = "POST";
/**
* token名称
*/
String X_ACCESS_TOKEN = "X-Access-Token";
/**
* 插件名称
*/
String PLUGIN_NAME = "流程调用";
/**
* 插件描述
*/
String PLUGIN_DESC = "调用工作流";
/**
* 插件请求地址
*/
String PLUGIN_REQUEST_URL = "/airag/flow/plugin/run/";
/**
* 记忆库插件名称
*/
String PLUGIN_MEMORY_NAME = "记忆库";
/**
* 记忆库插件描述
*/
String PLUGIN_MEMORY_DESC = "用于记录长期记忆";
/**
* 添加记忆路径
*/
String PLUGIN_MEMORY_ADD_PATH = "/airag/knowledge/plugin/add";
/**
* 查询记忆路径
*/
String PLUGIN_MEMORY_QUERY_PATH = "/airag/knowledge/plugin/query";
}

View File

@ -1,8 +1,5 @@
package org.jeecg.modules.airag.llm.consts; package org.jeecg.modules.airag.llm.consts;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@ -38,11 +35,6 @@ public class LLMConsts {
*/ */
public static final String MODEL_TYPE_LLM = "LLM"; public static final String MODEL_TYPE_LLM = "LLM";
/**
* 模型类型: 图像生成
*/
public static final String MODEL_TYPE_IMAGE = "IMAGE";
/** /**
* 向量模型:默认维度 * 向量模型:默认维度
*/ */
@ -93,29 +85,4 @@ public class LLMConsts {
*/ */
public static final String DEEPSEEK_REASONER = "deepseek-reasoner"; public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
/**
* 知识库类型:知识库
*/
public static final String KNOWLEDGE_TYPE_KNOWLEDGE = "knowledge";
/**
* 知识库类型:记忆库
*/
public static final String KNOWLEDGE_TYPE_MEMORY = "memory";
/**
* 支持文件的后缀
*/
public static final Set<String> CHAT_FILE_EXT_WHITELIST = new HashSet<>(Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md"));
/**
* 文件内容最大长度
*/
public static final int CHAT_FILE_TEXT_MAX_LENGTH = 20000;
/**
* 上传文件对打数量
*/
public static final int CHAT_FILE_MAX_COUNT = 3;
} }

View File

@ -1,31 +0,0 @@
package org.jeecg.modules.airag.llm.controller;
import org.jeecg.common.airag.api.IAiragBaseApi;
import org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* airag baseAPI Controller
*
* @author sjlei
* @date 2025-12-30
*/
@RestController("airagBaseApiController")
public class AiragBaseApiController implements IAiragBaseApi {
@Autowired
AiragBaseApiImpl airagBaseApi;
@PostMapping("/airag/api/knowledgeWriteTextDocument")
public String knowledgeWriteTextDocument(
@RequestParam("knowledgeId") String knowledgeId,
@RequestParam("title") String title,
@RequestParam("content") String content
) {
return airagBaseApi.knowledgeWriteTextDocument(knowledgeId, title, content);
}
}

View File

@ -1,18 +1,14 @@
package org.jeecg.modules.airag.llm.controller; package org.jeecg.modules.airag.llm.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils; import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig; import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult; import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
import org.jeecg.modules.airag.llm.consts.LLMConsts; import org.jeecg.modules.airag.llm.consts.LLMConsts;
@ -26,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -80,12 +77,9 @@ public class AiragKnowledgeController {
* @date 2025/2/18 17:09 * @date 2025/2/18 17:09
*/ */
@PostMapping(value = "/add") @PostMapping(value = "/add")
@RequiresPermissions("airag:knowledge:add") @SaCheckPermission("airag:knowledge:add")
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) { public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE); airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
if(oConvertUtils.isEmpty(airagKnowledge.getType())) {
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
}
airagKnowledgeService.save(airagKnowledge); airagKnowledgeService.save(airagKnowledge);
return Result.OK("添加成功!"); return Result.OK("添加成功!");
} }
@ -100,16 +94,13 @@ public class AiragKnowledgeController {
*/ */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:knowledge:edit") @SaCheckPermission("airag:knowledge:edit")
public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) { public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) {
AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId()); AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId());
if (airagKnowledgeEntity == null) { if (airagKnowledgeEntity == null) {
return Result.error("未找到对应数据"); return Result.error("未找到对应数据");
} }
String oldEmbedId = airagKnowledgeEntity.getEmbedId(); String oldEmbedId = airagKnowledgeEntity.getEmbedId();
if(oConvertUtils.isEmpty(airagKnowledgeEntity.getType())) {
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
}
airagKnowledgeService.updateById(airagKnowledge); airagKnowledgeService.updateById(airagKnowledge);
if (!oldEmbedId.equalsIgnoreCase(airagKnowledge.getEmbedId())) { if (!oldEmbedId.equalsIgnoreCase(airagKnowledge.getEmbedId())) {
// 更新了模型,重建文档 // 更新了模型,重建文档
@ -127,7 +118,7 @@ public class AiragKnowledgeController {
* @date 2025/3/12 17:05 * @date 2025/3/12 17:05
*/ */
@PutMapping(value = "/rebuild") @PutMapping(value = "/rebuild")
@RequiresPermissions("airag:knowledge:rebuild") @SaCheckPermission("airag:knowledge:rebuild")
public Result<?> rebuild(@RequestParam("knowIds") String knowIds) { public Result<?> rebuild(@RequestParam("knowIds") String knowIds) {
String[] knowIdArr = knowIds.split(","); String[] knowIdArr = knowIds.split(",");
for (String knowId : knowIdArr) { for (String knowId : knowIdArr) {
@ -146,7 +137,7 @@ public class AiragKnowledgeController {
*/ */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/delete") @DeleteMapping(value = "/delete")
@RequiresPermissions("airag:knowledge:delete") @SaCheckPermission("airag:knowledge:delete")
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) { public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------ //update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的 //如果是saas隔离的情况下判断当前租户id是否是当前租户下的
@ -213,7 +204,7 @@ public class AiragKnowledgeController {
* @date 2025/2/18 15:47 * @date 2025/2/18 15:47
*/ */
@PostMapping(value = "/doc/edit") @PostMapping(value = "/doc/edit")
@RequiresPermissions("airag:knowledge:doc:edit") @SaCheckPermission("airag:knowledge:doc:edit")
public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) { public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) {
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc); return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
} }
@ -226,7 +217,7 @@ public class AiragKnowledgeController {
* @date 2025/3/20 11:29 * @date 2025/3/20 11:29
*/ */
@PostMapping(value = "/doc/import/zip") @PostMapping(value = "/doc/import/zip")
@RequiresPermissions("airag:knowledge:doc:zip") @SaCheckPermission("airag:knowledge:doc:zip")
public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId, public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId,
@RequestParam(name = "file", required = true) MultipartFile file) { @RequestParam(name = "file", required = true) MultipartFile file) {
return airagKnowledgeDocService.importDocumentFromZip(knowId,file); return airagKnowledgeDocService.importDocumentFromZip(knowId,file);
@ -253,7 +244,7 @@ public class AiragKnowledgeController {
* @date 2025/2/18 15:47 * @date 2025/2/18 15:47
*/ */
@PutMapping(value = "/doc/rebuild") @PutMapping(value = "/doc/rebuild")
@RequiresPermissions("airag:knowledge:doc:rebuild") @SaCheckPermission("airag:knowledge:doc:rebuild")
public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) { public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) {
return airagKnowledgeDocService.rebuildDocument(docIds); return airagKnowledgeDocService.rebuildDocument(docIds);
} }
@ -268,7 +259,7 @@ public class AiragKnowledgeController {
*/ */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/doc/deleteBatch") @DeleteMapping(value = "/doc/deleteBatch")
@RequiresPermissions("airag:knowledge:doc:deleteBatch") @SaCheckPermission("airag:knowledge:doc:deleteBatch")
public Result<String> deleteDocumentBatch(HttpServletRequest request, @RequestParam(name = "ids", required = true) String ids) { public Result<String> deleteDocumentBatch(HttpServletRequest request, @RequestParam(name = "ids", required = true) String ids) {
List<String> idsList = Arrays.asList(ids.split(",")); List<String> idsList = Arrays.asList(ids.split(","));
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------ //update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
@ -296,7 +287,7 @@ public class AiragKnowledgeController {
*/ */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/doc/deleteAll") @DeleteMapping(value = "/doc/deleteAll")
@RequiresPermissions("airag:knowledge:doc:deleteAll") @SaCheckPermission("airag:knowledge:doc:deleteAll")
public Result<?> deleteDocumentAll(HttpServletRequest request, @RequestParam(name = "knowId") String knowId) { public Result<?> deleteDocumentAll(HttpServletRequest request, @RequestParam(name = "knowId") String knowId) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------ //update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的 //如果是saas隔离的情况下判断当前租户id是否是当前租户下的
@ -367,61 +358,4 @@ public class AiragKnowledgeController {
return Result.OK(airagKnowledges); return Result.OK(airagKnowledges);
} }
/**
* 添加记忆
*
* @param airagKnowledgeDoc
* @return
*/
@Operation(summary = "添加记忆")
@PostMapping(value = "/plugin/add")
public Result<?> add(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc, HttpServletRequest request) {
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getKnowledgeId())) {
return Result.error("知识库ID不能为空");
}
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getContent())) {
return Result.error("内容不能为空");
}
// 设置默认值
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getTitle())) {
// 取内容前20个字作为标题
String content = airagKnowledgeDoc.getContent();
String title = content.length() > 20 ? content.substring(0, 20) : content;
airagKnowledgeDoc.setTitle(title);
}
airagKnowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
// 保存并构建向量
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
}
/**
* 查询记忆
*
* @param params
* @return
*/
@Operation(summary = "查询记忆")
@PostMapping(value = "/plugin/query")
public Result<?> pluginQuery(@RequestBody Map<String, Object> params, HttpServletRequest request) {
String knowId = (String) params.get("knowledgeId");
String queryText = (String) params.get("queryText");
if (oConvertUtils.isEmpty(knowId)) {
return Result.error("知识库ID不能为空");
}
if (oConvertUtils.isEmpty(queryText)) {
return Result.error("查询内容不能为空");
}
LambdaQueryWrapper<AiragKnowledgeDoc> queryWrapper = new LambdaQueryWrapper<AiragKnowledgeDoc>();
queryWrapper.eq(AiragKnowledgeDoc::getKnowledgeId, knowId);
long count = airagKnowledgeDocService.count(queryWrapper);
if(count == 0){
return Result.ok("");
}
// 默认查询前5条
KnowledgeSearchResult searchResp = embeddingHandler.embeddingSearch(Collections.singletonList(knowId), queryText, (int) count, null);
return Result.ok(searchResp);
}
} }

View File

@ -169,7 +169,7 @@ public class AiragMcpController extends JeecgController<AiragMcp, IAiragMcpServi
* @param request * @param request
* @param airagMcp * @param airagMcp
*/ */
// @RequiresPermissions("llm:airag_mcp:exportXls") // @SaCheckPermission("llm:airag_mcp:exportXls")
@RequestMapping(value = "/exportXls") @RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, AiragMcp airagMcp) { public ModelAndView exportXls(HttpServletRequest request, AiragMcp airagMcp) {
return super.exportXls(request, airagMcp, AiragMcp.class, "MCP"); return super.exportXls(request, airagMcp, AiragMcp.class, "MCP");
@ -182,7 +182,7 @@ public class AiragMcpController extends JeecgController<AiragMcp, IAiragMcpServi
* @param response * @param response
* @return * @return
*/ */
// @RequiresPermissions("llm:airag_mcp:importExcel") // @SaCheckPermission("llm:airag_mcp:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST) @RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) { public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, AiragMcp.class); return super.importExcel(request, response, AiragMcp.class);

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.llm.controller; package org.jeecg.modules.airag.llm.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -7,7 +8,6 @@ import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.embedding.EmbeddingModel;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.ai.factory.AiModelFactory; import org.jeecg.ai.factory.AiModelFactory;
import org.jeecg.ai.factory.AiModelOptions; import org.jeecg.ai.factory.AiModelOptions;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
@ -17,7 +17,6 @@ import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig; import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.llm.consts.LLMConsts; import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragModel; import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.handler.AIChatHandler; import org.jeecg.modules.airag.llm.handler.AIChatHandler;
@ -73,7 +72,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
* @return * @return
*/ */
@PostMapping(value = "/add") @PostMapping(value = "/add")
@RequiresPermissions("airag:model:add") @SaCheckPermission("airag:model:add")
public Result<String> add(@RequestBody AiragModel airagModel) { public Result<String> add(@RequestBody AiragModel airagModel) {
// 验证 模型名称/模型类型/基础模型 // 验证 模型名称/模型类型/基础模型
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName()); AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
@ -96,7 +95,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
* @return * @return
*/ */
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:model:edit") @SaCheckPermission("airag:model:edit")
public Result<String> edit(@RequestBody AiragModel airagModel) { public Result<String> edit(@RequestBody AiragModel airagModel) {
airagModelService.updateById(airagModel); airagModelService.updateById(airagModel);
return Result.OK("编辑成功!"); return Result.OK("编辑成功!");
@ -109,7 +108,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
* @return * @return
*/ */
@DeleteMapping(value = "/delete") @DeleteMapping(value = "/delete")
@RequiresPermissions("airag:model:delete") @SaCheckPermission("airag:model:delete")
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) { public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------ //update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的 //如果是saas隔离的情况下判断当前租户id是否是当前租户下的
@ -173,16 +172,11 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
try { try {
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){ if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("To test whether it can be successfully called, simply return success")), null); aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("To test whether it can be successfully called, simply return success")), null);
}else if(LLMConsts.MODEL_TYPE_EMBED.equals(airagModel.getModelType())){ }else{
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel); AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions); EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
embeddingModel.embed("test text"); embeddingModel.embed("test text");
//update-begin---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---=
}else if(LLMConsts.MODEL_TYPE_IMAGE.equals(airagModel.getModelType())){
AIChatParams aiChatParams = new AIChatParams();
aiChatHandler.imageGenerate(airagModel, "To test whether it can be successfully called, simply return success", aiChatParams);
} }
//update-end---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---
}catch (Exception e){ }catch (Exception e){
log.error("测试模型连接失败", e); log.error("测试模型连接失败", e);
return Result.error("测试模型连接失败,请检查模型配置是否正确!"); return Result.error("测试模型连接失败,请检查模型配置是否正确!");

View File

@ -7,7 +7,6 @@ package org.jeecg.modules.airag.llm.document;
import dev.langchain4j.data.document.BlankDocumentException; import dev.langchain4j.data.document.BlankDocumentException;
import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.parser.apache.poi.ApachePoiDocumentParser;
import dev.langchain4j.internal.Utils; import dev.langchain4j.internal.Utils;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.apache.poi.hslf.usermodel.HSLFTextParagraph; import org.apache.poi.hslf.usermodel.HSLFTextParagraph;
@ -31,10 +30,7 @@ import org.xml.sax.ContentHandler;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -55,8 +51,6 @@ public class TikaDocumentParser {
private final Supplier<ContentHandler> contentHandlerSupplier; private final Supplier<ContentHandler> contentHandlerSupplier;
private final Supplier<Metadata> metadataSupplier; private final Supplier<Metadata> metadataSupplier;
private final Supplier<ParseContext> parseContextSupplier; private final Supplier<ParseContext> parseContextSupplier;
//文件前缀
private static final Set<String> FILE_SUFFIX = new HashSet<>(Arrays.asList("docx", "doc", "pptx", "ppt", "xlsx", "xls"));
public TikaDocumentParser() { public TikaDocumentParser() {
this((Supplier) ((Supplier) null), (Supplier) null, (Supplier) null, (Supplier) null); this((Supplier) ((Supplier) null), (Supplier) null, (Supplier) null, (Supplier) null);
@ -77,16 +71,22 @@ public class TikaDocumentParser {
InputStream isForParsing = Files.newInputStream(file.toPath()); InputStream isForParsing = Files.newInputStream(file.toPath());
// 使用 Tika 自动检测 MIME 类型 // 使用 Tika 自动检测 MIME 类型
String fileName = file.getName().toLowerCase(); String fileName = file.getName().toLowerCase();
//后缀
String ext = FilenameUtils.getExtension(fileName);
if (fileName.endsWith(".txt") if (fileName.endsWith(".txt")
|| fileName.endsWith(".md") || fileName.endsWith(".md")
|| fileName.endsWith(".pdf")) { || fileName.endsWith(".pdf")) {
return extractByTika(isForParsing); return extractByTika(isForParsing);
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档--- } else if (fileName.endsWith(".docx")) {
} else if (FILE_SUFFIX.contains(ext.toLowerCase())) { return extractTextFromDocx(isForParsing);
return parseDocExcelPdfUsingApachePoi(file); } else if (fileName.endsWith(".doc")) {
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档--- return extractTextFromDoc(isForParsing);
} else if (fileName.endsWith(".xlsx")) {
return extractTextFromExcel(isForParsing);
} else if (fileName.endsWith(".xls")) {
return extractTextFromExcel(isForParsing);
} else if (fileName.endsWith(".pptx")) {
return extractTextFromPptx(isForParsing);
} else if (fileName.endsWith(".ppt")) {
return extractTextFromPpt(isForParsing);
} else { } else {
throw new IllegalArgumentException("不支持的文件格式: " + FilenameUtils.getExtension(fileName)); throw new IllegalArgumentException("不支持的文件格式: " + FilenameUtils.getExtension(fileName));
} }
@ -95,27 +95,6 @@ public class TikaDocumentParser {
} }
} }
/**
* langchain4j 内部解析器
* @param file
* @return
*/
public Document parseDocExcelPdfUsingApachePoi(File file) {
AssertUtils.assertNotEmpty("请选择文件", file);
try (InputStream inputStream = Files.newInputStream(file.toPath())) {
ApachePoiDocumentParser parser = new ApachePoiDocumentParser();
Document document = parser.parse(inputStream);
if (document == null || Utils.isNullOrBlank(document.text())) {
return null;
}
return document;
} catch (BlankDocumentException e) {
return null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static Document tryExtractDocOrDocx(InputStream inputStream) throws IOException { private static Document tryExtractDocOrDocx(InputStream inputStream) throws IOException {
try { try {
// 先尝试 DOCX基于 OPC XML 格式) // 先尝试 DOCX基于 OPC XML 格式)

View File

@ -102,11 +102,4 @@ public class AiragKnowledge implements Serializable {
@Excel(name = "状态", width = 15) @Excel(name = "状态", width = 15)
@Schema(description = "状态") @Schema(description = "状态")
private java.lang.String status; private java.lang.String status;
/**
* 类型(knowledge知识 memory 记忆)
*/
@Excel(name="类型(knowledge知识 memory 记忆)", width = 15)
@Schema(description = "类型(knowledge知识 memory 记忆)")
private java.lang.String type;
} }

View File

@ -12,16 +12,13 @@ import org.jeecg.ai.handler.LLMHandler;
import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.AssertUtils; import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.handler.AIChatParams; import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler; import org.jeecg.modules.airag.common.handler.IAIChatHandler;
import org.jeecg.modules.airag.common.handler.McpToolProviderWrapper;
import org.jeecg.modules.airag.llm.consts.LLMConsts; import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragMcp; import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.entity.AiragModel; import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper; import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper; import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.jeecg.config.AiRagConfigBean;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -57,8 +54,6 @@ public class AIChatHandler implements IAIChatHandler {
@Autowired @Autowired
LLMHandler llmHandler; LLMHandler llmHandler;
@Autowired
AiRagConfigBean aiRagConfigBean;
@Value(value = "${jeecg.path.upload:}") @Value(value = "${jeecg.path.upload:}")
private String uploadpath; private String uploadpath;
@ -290,7 +285,7 @@ public class AIChatHandler implements IAIChatHandler {
// 默认超时时间 // 默认超时时间
if(oConvertUtils.isObjectEmpty(params.getTimeout())){ if(oConvertUtils.isObjectEmpty(params.getTimeout())){
params.setTimeout(AiragConsts.DEFAULT_TIMEOUT); params.setTimeout(60);
} }
//deepseek-reasoner 推理模型不支持插件tool //deepseek-reasoner 推理模型不支持插件tool
@ -306,7 +301,6 @@ public class AIChatHandler implements IAIChatHandler {
/** /**
* 构造插件和MCP工具 * 构造插件和MCP工具
* for [QQYUN-12453]【AI】支持插件 * for [QQYUN-12453]【AI】支持插件
* for [QQYUN-9234] MCP服务连接关闭 - 使用包装器保存连接引用
* @param params * @param params
* @author chenrui * @author chenrui
* @date 2025/10/31 14:04 * @date 2025/10/31 14:04
@ -316,7 +310,6 @@ public class AIChatHandler implements IAIChatHandler {
if(oConvertUtils.isObjectNotEmpty(pluginIds)){ if(oConvertUtils.isObjectNotEmpty(pluginIds)){
List<McpToolProvider> mcpToolProviders = new ArrayList<>(); List<McpToolProvider> mcpToolProviders = new ArrayList<>();
List<McpToolProviderWrapper> mcpToolProviderWrappers = new ArrayList<>();
Map<ToolSpecification, ToolExecutor> pluginTools = new HashMap<>(); Map<ToolSpecification, ToolExecutor> pluginTools = new HashMap<>();
for (String pluginId : pluginIds.stream().distinct().collect(Collectors.toList())) { for (String pluginId : pluginIds.stream().distinct().collect(Collectors.toList())) {
@ -332,18 +325,15 @@ public class AIChatHandler implements IAIChatHandler {
} }
if ("mcp".equalsIgnoreCase(category)) { if ("mcp".equalsIgnoreCase(category)) {
// MCP类型构建McpToolProviderWrapper包含连接引用用于后续关闭 // MCP类型构建McpToolProvider
// for [QQYUN-9234] MCP服务连接关闭 McpToolProvider mcpToolProvider = buildMcpToolProvider(
McpToolProviderWrapper wrapper = buildMcpToolProviderWrapper(
airagMcp.getName(), airagMcp.getName(),
airagMcp.getType(), airagMcp.getType(),
airagMcp.getEndpoint(), airagMcp.getEndpoint(),
airagMcp.getHeaders(), airagMcp.getHeaders()
aiRagConfigBean.getAllowSensitiveNodes()
); );
if (wrapper != null) { if (mcpToolProvider != null) {
mcpToolProviders.add(wrapper.getMcpToolProvider()); mcpToolProviders.add(mcpToolProvider);
mcpToolProviderWrappers.add(wrapper);
} }
} else if ("plugin".equalsIgnoreCase(category)) { } else if ("plugin".equalsIgnoreCase(category)) {
// 插件类型构建ToolSpecification和ToolExecutor // 插件类型构建ToolSpecification和ToolExecutor
@ -359,12 +349,6 @@ public class AIChatHandler implements IAIChatHandler {
params.setMcpToolProviders(mcpToolProviders); params.setMcpToolProviders(mcpToolProviders);
} }
// 保存MCP连接包装器用于后续关闭
// for [QQYUN-9234] MCP服务连接关闭
if (!mcpToolProviderWrappers.isEmpty()) {
params.setMcpToolProviderWrappers(mcpToolProviderWrappers);
}
// 设置插件工具 // 设置插件工具
if (!pluginTools.isEmpty()) { if (!pluginTools.isEmpty()) {
if (params.getTools() == null) { if (params.getTools() == null) {
@ -417,129 +401,5 @@ public class AIChatHandler implements IAIChatHandler {
return imageContents; return imageContents;
} }
//================================================= begin【QQYUN-12145】【AI】AI 绘画创作 ========================================
/**
* 文本生成图片
* @param modelId
* @param messages
* @param params
* @return
*/
@Override
public List<Map<String, Object>> imageGenerate(String modelId, String messages, AIChatParams params) {
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
AssertUtils.assertNotEmpty("请选择图片大模型", modelId);
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
return this.imageGenerate(airagModel, messages, params);
}
/**
* 文本生成图片
*
* @param airagModel
* @param messages
* @param params
* @return
*/
public List<Map<String, Object>> imageGenerate(AiragModel airagModel, String messages, AIChatParams params) {
params = mergeParams(airagModel, params);
try {
return llmHandler.imageGenerate(messages, params);
} catch (Exception e) {
String errMsg = "调用绘画AI接口失败详情请查看后台日志。";
if (oConvertUtils.isNotEmpty(e.getMessage())) {
// 根据常见异常关键字做细致翻译
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (errMsg.contains(key)) {
errMsg = value;
break;
}
}
}
log.error("AI模型调用异常: {}", errMsg, e);
throw new JeecgBootException(errMsg);
}
}
/**
* 图生图
*
* @param modelId
* @param messages
* @param images
* @param params
* @return
*/
@Override
public List<Map<String, Object>> imageEdit(String modelId, String messages, List<String> images, AIChatParams params) {
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
params = mergeParams(airagModel, params);
List<String> originalImageBase64List = getFirstImageBase64(images);
try {
return llmHandler.imageEdit(messages, originalImageBase64List, params);
} catch (Exception e) {
String errMsg = "调用绘画AI接口失败详情请查看后台日志。";
if (oConvertUtils.isNotEmpty(e.getMessage())) {
// 根据常见异常关键字做细致翻译
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (errMsg.contains(key)) {
errMsg = value;
break;
}
}
}
log.error("AI模型调用异常: {}", errMsg, e);
throw new JeecgBootException(errMsg);
}
}
/**
* 需要将图片转换成Base64编码
* @param images 图片路径列表
* @return Base64编码字符串
*/
private List<String> getFirstImageBase64(List<String> images) {
List<String> originalImageBase64List = new ArrayList<>();
if (images != null && !images.isEmpty()) {
for (String imageUrl : images) {
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(imageUrl);
try {
byte[] fileContent;
if (matcher.matches()) {
// 来源于网络
java.net.URL url = new java.net.URL(imageUrl);
java.net.URLConnection conn = url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
try (java.io.InputStream in = conn.getInputStream()) {
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
int nRead;
byte[] data = new byte[1024];
while ((nRead = in.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
fileContent = buffer.toByteArray();
}
} else {
// 本地文件
String filePath = uploadpath + File.separator + imageUrl;
Path path = Paths.get(filePath);
fileContent = Files.readAllBytes(path);
}
originalImageBase64List.add(Base64.getEncoder().encodeToString(fileContent));
} catch (Exception e) {
log.error("图片读取失败: " + imageUrl, e);
throw new JeecgBootException("图片读取失败: " + imageUrl);
}
}
}
return originalImageBase64List;
}
//================================================= end 【QQYUN-12145】【AI】AI 绘画创作 ========================================
} }

View File

@ -17,17 +17,13 @@ import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest; import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.filter.Filter;
import dev.langchain4j.store.embedding.filter.logical.And;
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore; import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.apache.tika.parser.AutoDetectParser; import org.apache.tika.parser.AutoDetectParser;
import org.jeecg.ai.factory.AiModelFactory; import org.jeecg.ai.factory.AiModelFactory;
import org.jeecg.ai.factory.AiModelOptions; import org.jeecg.ai.factory.AiModelOptions;
import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.*; import org.jeecg.common.util.*;
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler; import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult; import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
@ -98,21 +94,11 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/ */
private static final int DEFAULT_OVERLAP_SIZE = 50; private static final int DEFAULT_OVERLAP_SIZE = 50;
/**
* 最大输出长度
*/
private static final int DEFAULT_MAX_OUTPUT_CHARS = 4000;
/** /**
* 向量存储元数据:knowledgeId * 向量存储元数据:knowledgeId
*/ */
public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId"; public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId";
/**
* 向量存储元数据: 用户账号
*/
public static final String EMBED_STORE_METADATA_USER_NAME = "username";
/** /**
* 向量存储元数据:docId * 向量存储元数据:docId
*/ */
@ -123,11 +109,6 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/ */
public static final String EMBED_STORE_METADATA_DOCNAME = "docName"; public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
/**
* 向量存储元数据:创建时间
*/
public static final String EMBED_STORE_CREATE_TIME = "createTime";
/** /**
* 向量存储缓存 * 向量存储缓存
*/ */
@ -194,26 +175,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
.build(); .build();
Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId()) Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId())
.put(EMBED_STORE_METADATA_KNOWLEDGEID, doc.getKnowledgeId()) .put(EMBED_STORE_METADATA_KNOWLEDGEID, doc.getKnowledgeId())
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle())) .put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()));
//初始化记忆库的时候添加创建时间选项
.put(EMBED_STORE_CREATE_TIME, String.valueOf(doc.getCreateTime() != null ? doc.getCreateTime().getTime() : System.currentTimeMillis()));
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
//添加用户名字到元数据里面,用于记忆库中数据隔离
String username = doc.getCreateBy();
if (oConvertUtils.isEmpty(username)) {
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String token = TokenUtils.getTokenByRequest(request);
username = JwtUtil.getUsername(token);
} catch (Exception e) {
// ignoretoken获取不到默认为admin
username = "admin";
}
}
if (oConvertUtils.isNotEmpty(username)) {
metadata.put(EMBED_STORE_METADATA_USER_NAME, username);
}
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
Document from = Document.from(content, metadata); Document from = Document.from(content, metadata);
ingestor.ingest(from); ingestor.ingest(from);
return metadata.toMap(); return metadata.toMap();
@ -246,47 +208,16 @@ public class EmbeddingHandler implements IEmbeddingHandler {
} }
} }
//命中的文档内容
StringBuilder data = new StringBuilder(); StringBuilder data = new StringBuilder();
//update-begin---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字--- // 对documents按score降序排序并取前topNumber个
//是否为记忆库 List<Map<String, Object>> sortedDocuments = documents.stream()
boolean memoryMode = false; .sorted(Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
//记忆库只有一个 .limit(topNumber)
if (knowIds.size() == 1) { .peek(doc -> data.append(doc.get("content")).append("\n"))
String firstId = knowIds.get(0);
if (oConvertUtils.isNotEmpty(firstId)) {
AiragKnowledge k = airagKnowledgeMapper.getByIdIgnoreTenant(firstId);
memoryMode = (k != null && LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(k.getType()));
}
}
//如果是记忆库按照创建时间排序如果不是按照score分值进行排序
List<Map<String, Object>> prepared = documents.stream()
.sorted(memoryMode
? Comparator.comparingLong((Map<String, Object> doc) -> oConvertUtils.getLong(doc.get(EMBED_STORE_CREATE_TIME), 0L)).reversed()
: Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
.collect(Collectors.toList()); .collect(Collectors.toList());
List<Map<String, Object>> limited = new ArrayList<>();
//将返回的结果按照最大的token进行长度限制 return new KnowledgeSearchResult(data.toString(), sortedDocuments);
for (Map<String, Object> doc : prepared) {
if (limited.size() >= topNumber) {
break;
}
String content = oConvertUtils.getString(doc.get("content"), "");
int remain = DEFAULT_MAX_OUTPUT_CHARS - data.length();
if (remain <= 0) {
break;
}
//数据库中文本的长度和已经拼接的长度
if (content.length() <= remain) {
data.append(content).append("\n");
limited.add(doc);
} else {
data.append(content, 0, remain);
limited.add(doc);
break;
}
}
return new KnowledgeSearchResult(data.toString(), limited);
//update-end---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
} }
/** /**
@ -313,31 +244,11 @@ public class EmbeddingHandler implements IEmbeddingHandler {
topNumber = oConvertUtils.getInteger(topNumber, modelOp.getTopNumber()); topNumber = oConvertUtils.getInteger(topNumber, modelOp.getTopNumber());
similarity = oConvertUtils.getDou(similarity, modelOp.getSimilarity()); similarity = oConvertUtils.getDou(similarity, modelOp.getSimilarity());
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
// 记忆库的时候需要根据用户隔离
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String token = TokenUtils.getTokenByRequest(request);
String username = JwtUtil.getUsername(token);
if (oConvertUtils.isNotEmpty(username)) {
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
}
} catch (Exception e) {
// ignore
log.info("构建过滤器异常,{}",e.getMessage());
}
}
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder() EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding) .queryEmbedding(queryEmbedding)
.maxResults(topNumber) .maxResults(topNumber)
.minScore(similarity) .minScore(similarity)
.filter(filter) .filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
.build(); .build();
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model); EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
@ -351,9 +262,6 @@ public class EmbeddingHandler implements IEmbeddingHandler {
Metadata metadata = matchRes.embedded().metadata(); Metadata metadata = matchRes.embedded().metadata();
data.put("chunk", metadata.getInteger("index")); data.put("chunk", metadata.getInteger("index"));
data.put(EMBED_STORE_METADATA_DOCNAME, metadata.getString(EMBED_STORE_METADATA_DOCNAME)); data.put(EMBED_STORE_METADATA_DOCNAME, metadata.getString(EMBED_STORE_METADATA_DOCNAME));
//查询返回的时候增加创建时间,用于排序
String ct = metadata.getString(EMBED_STORE_CREATE_TIME);
data.put(EMBED_STORE_CREATE_TIME, ct);
return data; return data;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
@ -387,32 +295,13 @@ public class EmbeddingHandler implements IEmbeddingHandler {
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model); EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
topNumber = oConvertUtils.getInteger(topNumber, 5); topNumber = oConvertUtils.getInteger(topNumber, 5);
similarity = oConvertUtils.getDou(similarity, 0.75); similarity = oConvertUtils.getDou(similarity, 0.75);
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
// 记忆库的时候需要根据用户隔离
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String token = TokenUtils.getTokenByRequest(request);
String username = JwtUtil.getUsername(token);
if (oConvertUtils.isNotEmpty(username)) {
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
}
} catch (Exception e) {
// ignore
log.info("构建过滤器异常,{}",e.getMessage());
}
}
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
// 构建一个嵌入存储内容检索器,用于从嵌入存储中检索内容 // 构建一个嵌入存储内容检索器,用于从嵌入存储中检索内容
EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder() EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore) .embeddingStore(embeddingStore)
.embeddingModel(embeddingModel) .embeddingModel(embeddingModel)
.maxResults(topNumber) .maxResults(topNumber)
.minScore(similarity) .minScore(similarity)
.filter(filter) .filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
.build(); .build();
retrievers.add(contentRetriever); retrievers.add(contentRetriever);
} }

View File

@ -12,8 +12,6 @@ import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.RestUtil; import org.jeecg.common.util.RestUtil;
import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.flow.component.ToolsNode;
import org.jeecg.modules.airag.llm.entity.AiragMcp; import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -53,9 +51,8 @@ public class PluginToolBuilder {
} }
String baseUrl = airagMcp.getEndpoint(); String baseUrl = airagMcp.getEndpoint();
boolean isEmptyBaseUrl = oConvertUtils.isEmpty(baseUrl);
// 如果baseUrl为空使用当前系统地址 // 如果baseUrl为空使用当前系统地址
if (isEmptyBaseUrl) { if (oConvertUtils.isEmpty(baseUrl)) {
if (currentHttpRequest != null) { if (currentHttpRequest != null) {
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest); baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
log.info("插件[{}]的BaseURL为空使用系统地址: {}", airagMcp.getName(), baseUrl); log.info("插件[{}]的BaseURL为空使用系统地址: {}", airagMcp.getName(), baseUrl);
@ -68,9 +65,6 @@ public class PluginToolBuilder {
// 解析headers // 解析headers
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders()); Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
// 判断是否需要加签
boolean isNeedSign = isEmptyBaseUrl && ToolsNode.Helper.checkNeedSign(headersMap);
// 解析并应用授权配置从metadata中读取 // 解析并应用授权配置从metadata中读取
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest); applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
@ -82,7 +76,7 @@ public class PluginToolBuilder {
try { try {
ToolSpecification spec = buildToolSpecification(toolConfig); ToolSpecification spec = buildToolSpecification(toolConfig);
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap, isNeedSign); ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap);
if (spec != null && executor != null) { if (spec != null && executor != null) {
tools.put(spec, executor); tools.put(spec, executor);
} }
@ -193,7 +187,7 @@ public class PluginToolBuilder {
/** /**
* 构建ToolExecutor * 构建ToolExecutor
*/ */
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders, boolean isNeedSign) { private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders) {
String path = toolConfig.getString("path"); String path = toolConfig.getString("path");
String method = toolConfig.getString("method"); String method = toolConfig.getString("method");
JSONArray parameters = toolConfig.getJSONArray("parameters"); JSONArray parameters = toolConfig.getJSONArray("parameters");
@ -221,13 +215,8 @@ public class PluginToolBuilder {
JSONObject urlVariables = buildUrlVariables(parameters, args); JSONObject urlVariables = buildUrlVariables(parameters, args);
Object body = buildRequestBody(parameters, args, httpHeaders); Object body = buildRequestBody(parameters, args, httpHeaders);
if (isNeedSign) { // 发送HTTP请求
// 发送请求前加签 ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class);
ToolsNode.Helper.applySignature(url, httpHeaders, urlVariables, body);
}
// 发送HTTP请求,增加超时时间
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class, AiragConsts.DEFAULT_TIMEOUT * 1000);
// 直接返回原始响应字符串,不进行解析 // 直接返回原始响应字符串,不进行解析
return response.getBody() != null ? response.getBody() : ""; return response.getBody() != null ? response.getBody() : "";
@ -346,13 +335,7 @@ public class PluginToolBuilder {
if (isQueryParam || !isOtherType) { if (isQueryParam || !isOtherType) {
Object value = args.get(paramName); Object value = args.get(paramName);
if (value != null) { if (value != null) {
//如果是知识库的id赋值默认值 urlVariables.put(paramName, value);
if ("knowledgeId".equalsIgnoreCase(paramName)) {
String defaultValue = param.getString("defaultValue");
urlVariables.put(paramName, defaultValue);
} else {
urlVariables.put(paramName, value);
}
} }
} }
} }
@ -409,13 +392,7 @@ public class PluginToolBuilder {
Object value = args.get(paramName); Object value = args.get(paramName);
if (value != null) { if (value != null) {
//如果是知识库的id赋值默认值 body.put(paramName, value);
if ("knowledgeId".equalsIgnoreCase(paramName)) {
String defaultValue = param.getString("defaultValue");
body.put(paramName, defaultValue);
} else {
body.put(paramName, value);
}
} else { } else {
// 检查是否有默认值 // 检查是否有默认值
String defaultValue = param.getString("defaultValue"); String defaultValue = param.getString("defaultValue");

View File

@ -1,19 +0,0 @@
package org.jeecg.modules.airag.llm.service;
import java.util.Map;
/**
* @Description: 获取流程mcp服务
* @Author: wangshuai
* @Date: 2025-12-22 15:34:20
* @Version: V1.0
*/
public interface IAiragFlowPluginService {
/**
* 同步所有启用的流程到MCP插件配置
*
* @param flowIds 多个流程id
*/
Map<String, Object> getFlowsToPlugin(String flowIds);
}

View File

@ -3,8 +3,6 @@ package org.jeecg.modules.airag.llm.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge; import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import java.util.Map;
/** /**
* AIRag知识库 * AIRag知识库
* *
@ -13,12 +11,4 @@ import java.util.Map;
* @Version: V1.0 * @Version: V1.0
*/ */
public interface IAiragKnowledgeService extends IService<AiragKnowledge> { public interface IAiragKnowledgeService extends IService<AiragKnowledge> {
/**
* 构建知识库的工具
*
* @param memoryId
* @return Map<String, Object>
*/
Map<String, Object> getPluginMemory(String memoryId);
} }

View File

@ -1,45 +0,0 @@
package org.jeecg.modules.airag.llm.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.airag.api.IAiragBaseApi;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
/**
* airag baseAPI 实现类
*/
@Slf4j
@Primary
@Service("airagBaseApiImpl")
public class AiragBaseApiImpl implements IAiragBaseApi {
@Autowired
private IAiragKnowledgeDocService airagKnowledgeDocService;
@Override
public String knowledgeWriteTextDocument(String knowledgeId, String title, String content) {
AssertUtils.assertNotEmpty("知识库ID不能为空", knowledgeId);
AssertUtils.assertNotEmpty("写入内容不能为空", content);
AiragKnowledgeDoc knowledgeDoc = new AiragKnowledgeDoc();
knowledgeDoc.setKnowledgeId(knowledgeId);
knowledgeDoc.setTitle(title);
knowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
knowledgeDoc.setContent(content);
Result<?> result = airagKnowledgeDocService.editDocument(knowledgeDoc);
if (!result.isSuccess()) {
throw new JeecgBootBizTipException(result.getMessage());
}
if (knowledgeDoc.getId() == null) {
throw new JeecgBootBizTipException("知识库文档ID为空");
}
log.info("[AI-KNOWLEDGE] 文档写入完成,知识库:{}, 文档ID:{}", knowledgeId, knowledgeDoc.getId());
return knowledgeDoc.getId();
}
}

View File

@ -1,231 +0,0 @@
package org.jeecg.modules.airag.llm.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.service.tool.ToolExecutor;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* @Description: 流程同步到MCP服务实现类
* @Author: wangshuai
* @Date: 2025-12-22
* @Version: V1.0
*/
@Service
@Slf4j
public class AiragFlowPluginServiceImpl implements IAiragFlowPluginService {
@Autowired
private IAiragFlowService airagFlowService;
@Override
public Map<String, Object> getFlowsToPlugin(String flowIds) {
log.info("开始构建流程插件");
// 1. 查询所有启用的流程
LambdaQueryWrapper<AiragFlow> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AiragFlow::getStatus, FlowConsts.FLOW_STATUS_ENABLE);
queryWrapper.in(AiragFlow::getId, Arrays.asList(flowIds.split(SymbolConstant.COMMA)));
List<AiragFlow> flows = airagFlowService.list(queryWrapper);
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
if (flows.isEmpty()) {
log.info("当前应用所选流程没有启用的流程");
return null;
}
//返回数据
Map<String, Object> result = new HashMap<>();
//插件
//插件id
AiragMcp tool = new AiragMcp();
// 2. 构建插件
String id = UUID.randomUUID().toString().replace("-", "");
tool.setId(id);
// 插件名称
tool.setName(FlowPluginContent.PLUGIN_NAME);
// 描述
tool.setDescr(FlowPluginContent.PLUGIN_DESC);
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
tool.setSynced(CommonConstant.STATUS_1_INT);
tool.setCategory("plugin");
tool.setEndpoint("");
int toolCount = 0;
//构建拆件工具
for (AiragFlow flow : flows) {
try {
SubFlowResult subFlow = new SubFlowResult(flow);
// 获取入参参数
JSONArray parameter = getInputParameter(flow, subFlow);
// 获取出参参数
JSONArray outParams = getOutputParameter(flow, subFlow);
// name必须符合 ^[a-zA-Z0-9_-]+$
String validToolName = "flow_" + flow.getId();
// 将原始名称拼接到描述中
String description = flow.getName();
if (oConvertUtils.isNotEmpty(flow.getDescr())) {
description += " : " + flow.getDescr();
}
//构造工具参数
String flowTool = buildParameter(parameter, outParams, flow.getId(), tool.getTools(), validToolName, description);
tool.setTools(flowTool);
toolCount++;
} catch (Exception e) {
log.error("处理流程[{}]转换插件失败: {}", flow.getName(), e.getMessage());
}
}
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
//构建元数据(请求头)
String meataData = buildMetadata(toolCount, tenantId);
tool.setMetadata(meataData);
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
result.put("pluginTool", tools);
result.put("pluginId", id);
log.info("构建流程插件结束");
return result;
}
/**
* 构建元数据
*
* @param toolCount
* @param tenantId
*/
private String buildMetadata(int toolCount, String tenantId) {
JSONObject jsonObject = new JSONObject();
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
jsonObject.put(FlowPluginContent.TOOL_COUNT, toolCount);
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
return jsonObject.toJSONString();
}
/**
* 构建参数
*
* @param parameter
* @param outParams
* @param flowId
* @param tools
* @param description
* @param name
*/
private String buildParameter(JSONArray parameter, JSONArray outParams, String flowId, String tools, String name, String description) {
JSONArray paramArray = new JSONArray();
JSONObject parameterObject = new JSONObject();
parameterObject.put(FlowPluginContent.NAME, name);
parameterObject.put(FlowPluginContent.DESCRIPTION, description);
parameterObject.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_REQUEST_URL + flowId);
parameterObject.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
parameterObject.put(FlowPluginContent.ENABLED, true);
parameterObject.put(FlowPluginContent.PARAMETERS, parameter);
parameterObject.put(FlowPluginContent.RESPONSES, outParams);
if (oConvertUtils.isNotEmpty(tools)) {
paramArray = JSONArray.parseArray(tools);
paramArray.add(parameterObject);
} else {
paramArray.add(parameterObject);
}
return paramArray.toJSONString();
}
/**
* 获取参数
*
* @param flow
* @param subFlow
*/
private JSONArray getInputParameter(AiragFlow flow, SubFlowResult subFlow) {
JSONArray parameters = new JSONArray();
String metadata = flow.getMetadata();
if (oConvertUtils.isNotEmpty(metadata)) {
JSONObject jsonObject = JSONObject.parseObject(metadata);
if (jsonObject.containsKey(FlowPluginContent.INPUTS)) {
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.INPUTS);
jsonArray.forEach(item -> {
if (oConvertUtils.isNotEmpty(item.toString())) {
JSONObject json = JSONObject.parseObject(item.toString());
json.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
}
});
parameters.addAll(jsonArray);
}
}
//需要获取子流程的参数,子流程的参数是单独封装的,否则在流程执行的时候会报错缺少参数
List<FlowNodeConfig.NodeParam> inputParams = subFlow.getInputParams();
if (inputParams != null) {
for (FlowNodeConfig.NodeParam param : inputParams) {
JSONObject p = new JSONObject();
// 参数名
p.put(FlowPluginContent.NAME, param.getField());
String paramDesc = param.getName();
if (oConvertUtils.isEmpty(paramDesc)) {
paramDesc = param.getField();
}
// 参数描述
p.put(FlowPluginContent.DESCRIPTION, paramDesc);
// 类型
p.put(FlowPluginContent.TYPE, oConvertUtils.getString(param.getType(), FlowPluginContent.TYPE_STRING));
// 所有参数都在Body中
p.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
boolean required = param.getRequired() != null && param.getRequired();
p.put(FlowPluginContent.REQUIRED, required);
parameters.add(p);
}
}
return parameters;
}
/**
* 构建返回值
*/
private JSONArray getOutputParameter(AiragFlow flow, SubFlowResult subFlow) {
JSONArray parameters = new JSONArray();
String metadata = flow.getMetadata();
if (oConvertUtils.isNotEmpty(metadata)) {
JSONObject jsonObject = JSONObject.parseObject(metadata);
if (jsonObject.containsKey(FlowPluginContent.OUTPUTS)) {
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.OUTPUTS);
parameters.addAll(jsonArray);
}
}
// List<FlowNodeConfig.NodeParam> outputParams = subFlow.getOutputParams();
// if (outputParams != null) {
// for (FlowNodeConfig.NodeParam param : outputParams) {
// JSONObject p = new JSONObject();
// // 参数名
// p.put("name", param.getField());
// String paramDesc = param.getName();
// if (oConvertUtils.isEmpty(paramDesc)) {
// paramDesc = param.getField();
// }
// // 参数描述
// p.put("description", paramDesc);
// // 类型
// p.put("type", oConvertUtils.getString(param.getType(), "String"));
// parameters.add(p);
// }
// }
return parameters;
}
}

View File

@ -131,6 +131,7 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList); List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList);
AssertUtils.assertNotEmpty("文档不存在", docList); AssertUtils.assertNotEmpty("文档不存在", docList);
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
// 检查状态 // 检查状态
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream() List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
.filter(doc -> { .filter(doc -> {
@ -329,7 +330,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
if (File.separator.equals("\\")) { if (File.separator.equals("\\")) {
// Windows path handling // Windows path handling
String escapedPath = uploadpath.replace("//", "\\\\"); String escapedPath = uploadpath.replace("//", "\\\\");
escapedPath = escapedPath.replace("/", "\\\\");
relativePath = uploadedFile.getPath().replaceFirst("^" + escapedPath, ""); relativePath = uploadedFile.getPath().replaceFirst("^" + escapedPath, "");
} else { } else {
// Unix path handling // Unix path handling

View File

@ -1,215 +1,18 @@
package org.jeecg.modules.airag.llm.service.impl; package org.jeecg.modules.airag.llm.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.service.tool.ToolExecutor;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge; import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper; import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService; import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/** /**
* @Description: AIRag知识库 * @Description: AIRag知识库
* @Author: jeecg-boot * @Author: jeecg-boot
* @Date: 2025-02-18 * @Date: 2025-02-18
* @Version: V1.0 * @Version: V1.0
*/ */
@Slf4j
@Service @Service
public class AiragKnowledgeServiceImpl extends ServiceImpl<AiragKnowledgeMapper, AiragKnowledge> implements IAiragKnowledgeService { public class AiragKnowledgeServiceImpl extends ServiceImpl<AiragKnowledgeMapper, AiragKnowledge> implements IAiragKnowledgeService {
@Override
public Map<String, Object> getPluginMemory(String memoryId) {
//step 1获取知识库
AiragKnowledge airagKnowledge = this.baseMapper.selectById(memoryId);
if(airagKnowledge == null){
return null;
}
return this.getKnowledgeToPlugin(memoryId,airagKnowledge.getDescr());
}
/**
* 获取插件信息
*
* @param knowledgeId
* @param descr
* @return
*/
public Map<String, Object> getKnowledgeToPlugin(String knowledgeId, String descr) {
//step1 构建插件
log.info("开始构建记忆库插件");
if (oConvertUtils.isEmpty(knowledgeId)) {
return null;
}
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
//返回数据
Map<String, Object> result = new HashMap<>();
//插件
//插件id
AiragMcp tool = new AiragMcp();
// 2. 构建插件
tool.setId(knowledgeId);
// 插件名称
tool.setName(FlowPluginContent.PLUGIN_MEMORY_NAME);
// 描述
tool.setDescr(FlowPluginContent.PLUGIN_MEMORY_DESC);
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
tool.setSynced(CommonConstant.STATUS_1_INT);
tool.setCategory("plugin");
tool.setEndpoint("");
JSONArray toolsArray = new JSONArray();
// 添加记忆
toolsArray.add(buildAddMemoryTool(knowledgeId,descr));
// 查询记忆
toolsArray.add(buildQueryMemoryTool(knowledgeId,descr));
tool.setTools(toolsArray.toJSONString());
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
//构建元数据(请求头)
String meataData = buildMetadata(tenantId);
tool.setMetadata(meataData);
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
result.put("pluginTool", tools);
result.put("pluginId", knowledgeId);
log.info("构建记忆库插件结束");
return result;
}
/**
* 构建元数据
*
* @param tenantId
*/
private String buildMetadata(String tenantId) {
JSONObject jsonObject = new JSONObject();
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
return jsonObject.toJSONString();
}
/**
* 构建添加记忆工具
*
* @param knowId
* @param descr
* @return
*/
private JSONObject buildAddMemoryTool(String knowId, String descr) {
JSONObject tool = new JSONObject();
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
tool.put(FlowPluginContent.NAME, "add_memory");
String addDescPrefix = "【自动触发】向记忆库添加长期信息。范围:";
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_ADD_PATH);
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
tool.put(FlowPluginContent.ENABLED, true);
JSONArray parameters = new JSONArray();
// 知识库ID参数
JSONObject knowIdParam = new JSONObject();
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
knowIdParam.put(FlowPluginContent.REQUIRED, true);
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
parameters.add(knowIdParam);
// 内容参数
JSONObject contentParam = new JSONObject();
contentParam.put(FlowPluginContent.NAME, "content");
contentParam.put(FlowPluginContent.DESCRIPTION, "记忆内容。当前时间为:" + DateUtils.now() + "。格式要求:'在yyyy年MM月dd日 HH:mm分用户[用户的行为/问题]assistant[助手的回答/反应]。'");
contentParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
contentParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
contentParam.put(FlowPluginContent.REQUIRED, true);
parameters.add(contentParam);
// 标题参数
JSONObject titleParam = new JSONObject();
titleParam.put(FlowPluginContent.NAME, "title");
titleParam.put(FlowPluginContent.DESCRIPTION, "记忆标题");
titleParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
titleParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
titleParam.put(FlowPluginContent.REQUIRED, false);
parameters.add(titleParam);
tool.put(FlowPluginContent.PARAMETERS, parameters);
// 响应
JSONArray responses = new JSONArray();
tool.put(FlowPluginContent.RESPONSES, responses);
return tool;
}
/**
* 构建查询记忆工具
*
* @param knowId
* @param descr
* @return
*/
private JSONObject buildQueryMemoryTool(String knowId, String descr) {
JSONObject tool = new JSONObject();
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
String addDescPrefix = "【自动触发】向记忆库检索信息。范围:";
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
tool.put(FlowPluginContent.NAME, "query_memory");
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_QUERY_PATH);
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
tool.put(FlowPluginContent.ENABLED, true);
JSONArray parameters = new JSONArray();
// 知识库ID参数
JSONObject knowIdParam = new JSONObject();
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
knowIdParam.put(FlowPluginContent.REQUIRED, true);
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
parameters.add(knowIdParam);
// 查询内容参数
JSONObject queryTextParam = new JSONObject();
queryTextParam.put(FlowPluginContent.NAME, "queryText");
queryTextParam.put(FlowPluginContent.DESCRIPTION, "查询内容");
queryTextParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
queryTextParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
queryTextParam.put(FlowPluginContent.REQUIRED, true);
parameters.add(queryTextParam);
tool.put(FlowPluginContent.PARAMETERS, parameters);
// 响应
JSONArray responses = new JSONArray();
tool.put(FlowPluginContent.RESPONSES, responses);
return tool;
}
} }

View File

@ -10,14 +10,11 @@ import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.mcp.client.DefaultMcpClient; import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient; import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport; import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import dev.langchain4j.model.chat.request.json.*; import dev.langchain4j.model.chat.request.json.*;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.AiRagConfigBean;
import org.jeecg.modules.airag.llm.entity.AiragMcp; import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper; import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
import org.jeecg.modules.airag.llm.service.IAiragMcpService; import org.jeecg.modules.airag.llm.service.IAiragMcpService;
@ -25,7 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -35,15 +31,12 @@ import java.util.stream.Collectors;
* @Date: 2025-10-20 * @Date: 2025-10-20
* @Version: V1.0 * @Version: V1.0
*/ */
@Slf4j
@SuppressWarnings("removal")
@Service("airagMcpServiceImpl") @Service("airagMcpServiceImpl")
@Slf4j
public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService { public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService {
@Autowired @Autowired
private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper
@Autowired
private AiRagConfigBean aiRagConfigBean;
/** /**
* 新增或编辑Mcpserver * 新增或编辑Mcpserver
@ -132,7 +125,7 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
Map<String, String> headers = null; Map<String, String> headers = null;
if (oConvertUtils.isNotEmpty(mcp.getHeaders())) { if (oConvertUtils.isNotEmpty(mcp.getHeaders())) {
try { try {
headers = JSONObject.parseObject(mcp.getHeaders(), new com.alibaba.fastjson.TypeReference<Map<String, String>>() {}); headers = JSONObject.parseObject(mcp.getHeaders(), Map.class);
} catch (JSONException e) { } catch (JSONException e) {
headers = null; headers = null;
} }
@ -143,53 +136,19 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
McpClient mcpClient = null; McpClient mcpClient = null;
try { try {
if ("sse".equalsIgnoreCase(type)) { if ("sse".equalsIgnoreCase(type)) {
//TODO 1.4.0-beta10被弃用,推荐使用http HttpMcpTransport.Builder builder = new HttpMcpTransport.Builder()
log.info("[MCP]使用SSE协议(HttpMcpTransport), endpoint:{}", endpoint);
HttpMcpTransport.Builder builder = HttpMcpTransport.builder()
.sseUrl(endpoint) .sseUrl(endpoint)
.logRequests(true) .logRequests(true)
.logResponses(true); .logResponses(true);
if (headers != null && !headers.isEmpty()) {
builder.customHeaders(headers);
}
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build(); mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
} else if ("stdio".equalsIgnoreCase(type)) { } else if ("stdio".equalsIgnoreCase(type)) {
//update-begin---author:wangshuai---date:2025-12-18---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令--- // stdio 类型endpoint 可能是一个命令行,需要拆分为命令列表
String openSafe = aiRagConfigBean.getAllowSensitiveNodes(); // List<String> cmdParts = Arrays.asList(endpoint.trim().split("\\s+"));
if(oConvertUtils.isNotEmpty(openSafe) && openSafe.toLowerCase().contains("stdio")) { // StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
log.info("[MCP]使用STDIO协议(StdioMcpTransport), endpoint:{}", endpoint); // .command(cmdParts)
// stdio 类型endpoint 可能是一个命令行 // .environment(headers);
// Windows 下需要通过 cmd.exe /c 来执行命令,否则找不到 npx 等程序 // mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
List<String> cmdParts; return Result.error("不支持的MCP类型:" + type);
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
// Windows: 使用 cmd.exe /c 执行
cmdParts = new ArrayList<>();
cmdParts.add("cmd.exe");
cmdParts.add("/c");
cmdParts.add(endpoint.trim());
} else {
// Linux/Mac: 使用 sh -c 执行
cmdParts = new ArrayList<>();
cmdParts.add("sh");
cmdParts.add("-c");
cmdParts.add(endpoint.trim());
}
log.info("[MCP]执行stdio命令: {}", cmdParts);
StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
.command(cmdParts)
.environment(headers);
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
} else {
String disabledMsg = "stdio 功能已禁用。若需启用,请在 yml 的 jeecg.airag.allow-sensitive-nodes 中加入 stdio。";
log.warn("[MCP]{}", disabledMsg);
return Result.error(disabledMsg);
}
//update-end---author:wangshuai---date:2025-12-19---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
}else if("http".equalsIgnoreCase(type)){
log.info("[MCP]使用HTTP协议(StreamableHttpMcpTransport), endpoint:{}", endpoint);
//增加http选项
mcpClient = mcpHttpCreate(endpoint,headers);
} else { } else {
return Result.error("不支持的MCP类型:" + type); return Result.error("不支持的MCP类型:" + type);
} }
@ -245,29 +204,6 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
} }
} }
/**
* mcp插件http创建
*
* @param endpoint
* @param headers
* @return
*/
private McpClient mcpHttpCreate(String endpoint, Map<String, String> headers) {
StreamableHttpMcpTransport.Builder builder = new StreamableHttpMcpTransport.Builder()
.url(endpoint)
.timeout(Duration.ofMinutes(60))
.logRequests(true)
.logResponses(true);
if (headers != null && !headers.isEmpty()) {
builder.customHeaders(headers);
}
return new DefaultMcpClient.Builder()
.transport(builder.build())
.build();
}
// 安全序列化单个对象为 JSON 字符串 // 安全序列化单个对象为 JSON 字符串
private String safeWriteJson(Object obj) { private String safeWriteJson(Object obj) {
try { try {

View File

@ -1,30 +0,0 @@
package org.jeecg.modules.airag.prompts.consts;
/**
* AI提示词常量类
*
*/
public class AiPromptsConsts {
/**
* 状态:进行中
*/
public static final String STATUS_RUNNING = "run";
/**
* 状态:完成
*/
public static final String STATUS_COMPLETED = "completed";
/**
* 状态:失败
*/
public static final String STATUS_FAILED = "failed";
/**
* 业务类型:评估器
*/
public static final String BIZ_TYPE_EVALUATOR = "evaluator";
/**
* 业务类型:轨迹
*/
public static final String BIZ_TYPE_TRACK = "track";
}

View File

@ -1,213 +0,0 @@
package org.jeecg.modules.airag.prompts.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
import java.util.List;
/**
* @Description: airag_ext_data
* @Author: jeecg-boot
* @Date: 2025-12-24
* @Version: V1.0
*/
@Tag(name="airag_ext_data")
@RestController
@RequestMapping("/airag/extData")
@Slf4j
public class AiragExtDataController extends JeecgController<AiragExtData, IAiragExtDataService> {
@Autowired
private IAiragExtDataService airagExtDataService;
/**
* 分页列表查询
*
* @param airagExtData
* @param pageNo
* @param pageSize
* @param req
* @return
*/
//@AutoLog(value = "airag_ext_data-分页列表查询")
@Operation(summary="airag_ext_data-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<AiragExtData>> queryPageList(AiragExtData airagExtData,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_EVALUATOR);
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 调用轨迹列表查询
*
* @param airagExtData
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@Operation(summary="airag_ext_data-分页列表查询")
@GetMapping(value = "/getTrackList")
public Result<IPage<AiragExtData>> getTrackList(AiragExtData airagExtData,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_TRACK);
String metadata = airagExtData.getMetadata();
if(oConvertUtils.isEmpty(metadata)){
return Result.OK();
}
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param airagExtData
* @return
*/
@AutoLog(value = "airag_ext_data-添加")
@Operation(summary="airag_ext_data-添加")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody AiragExtData airagExtData) {
airagExtData.setBizType(AiPromptsConsts.BIZ_TYPE_EVALUATOR);
airagExtDataService.save(airagExtData);
return Result.OK("添加成功!");
}
/**
* 编辑
*
* @param airagExtData
* @return
*/
@AutoLog(value = "airag_ext_data-编辑")
@Operation(summary="airag_ext_data-编辑")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> edit(@RequestBody AiragExtData airagExtData) {
airagExtDataService.updateById(airagExtData);
return Result.OK("编辑成功!");
}
/**
* 通过id删除
*
* @param id
* @return
*/
@AutoLog(value = "airag_ext_data-通过id删除")
@Operation(summary="airag_ext_data-通过id删除")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
airagExtDataService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@AutoLog(value = "airag_ext_data-批量删除")
@Operation(summary="airag_ext_data-批量删除")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
this.airagExtDataService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "airag_ext_data-通过id查询")
@Operation(summary="airag_ext_data-通过id查询")
@GetMapping(value = "/queryById")
public Result<AiragExtData> queryById(@RequestParam(name="id",required=true) String id) {
AiragExtData airagExtData = airagExtDataService.getById(id);
if(airagExtData==null) {
return Result.error("未找到对应数据");
}
return Result.OK(airagExtData);
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "airag_ext_data-通过id查询")
@Operation(summary="airag_ext_data-通过id查询")
@GetMapping(value = "/queryTrackById")
public Result<List<AiragExtData>> queryTrackById(@RequestParam(name="id",required=true) String id) {
AiragExtData airagExtData = airagExtDataService.getById(id);
String status = airagExtData.getStatus();
if(AiPromptsConsts.STATUS_RUNNING.equals(status)) {
return Result.error("处理中,请稍后刷新");
}
List<AiragExtData> trackList = airagExtDataService.queryTrackById(id);
return Result.OK(trackList);
}
/**
* 构造器调试
*
* @param debugVo
* @return
*/
@PostMapping(value = "/evaluator/debug")
public Result<?> debugEvaluator(@RequestBody AiragDebugVo debugVo) {
return airagExtDataService.debugEvaluator(debugVo);
}
/**
* 导出excel
*
* @param request
* @param airagExtData
*/
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, AiragExtData airagExtData) {
return super.exportXls(request, airagExtData, AiragExtData.class, "airag_ext_data");
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, AiragExtData.class);
}
}

View File

@ -1,167 +0,0 @@
package org.jeecg.modules.airag.prompts.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
/**
* @Description: airag_prompts
* @Author: jeecg-boot
* @Date: 2025-12-24
* @Version: V1.0
*/
@Tag(name="airag_prompts")
@RestController
@RequestMapping("/airag/prompts")
@Slf4j
public class AiragPromptsController extends JeecgController<AiragPrompts, IAiragPromptsService> {
@Autowired
private IAiragPromptsService airagPromptsService;
/**
* 分页列表查询
*
* @param airagPrompts
* @param pageNo
* @param pageSize
* @param req
* @return
*/
//@AutoLog(value = "airag_prompts-分页列表查询")
@Operation(summary="airag_prompts-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<AiragPrompts>> queryPageList(AiragPrompts airagPrompts,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<AiragPrompts> queryWrapper = QueryGenerator.initQueryWrapper(airagPrompts, req.getParameterMap());
Page<AiragPrompts> page = new Page<AiragPrompts>(pageNo, pageSize);
IPage<AiragPrompts> pageList = airagPromptsService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param airagPrompts
* @return
*/
@AutoLog(value = "airag_prompts-添加")
@Operation(summary="airag_prompts-添加")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody AiragPrompts airagPrompts) {
airagPrompts.setDelFlag(CommonConstant.DEL_FLAG_0);
airagPrompts.setStatus("0");
airagPromptsService.save(airagPrompts);
return Result.OK("添加成功!");
}
/**
* 编辑
*
* @param airagPrompts
* @return
*/
@AutoLog(value = "airag_prompts-编辑")
@Operation(summary="airag_prompts-编辑")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> edit(@RequestBody AiragPrompts airagPrompts) {
airagPromptsService.updateById(airagPrompts);
return Result.OK("编辑成功!");
}
/**
* 通过id删除
*
* @param id
* @return
*/
@AutoLog(value = "airag_prompts-通过id删除")
@Operation(summary="airag_prompts-通过id删除")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
airagPromptsService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@AutoLog(value = "airag_prompts-批量删除")
@Operation(summary="airag_prompts-批量删除")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
this.airagPromptsService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "airag_prompts-通过id查询")
@Operation(summary="airag_prompts-通过id查询")
@GetMapping(value = "/queryById")
public Result<AiragPrompts> queryById(@RequestParam(name="id",required=true) String id) {
AiragPrompts airagPrompts = airagPromptsService.getById(id);
if(airagPrompts==null) {
return Result.error("未找到对应数据");
}
return Result.OK(airagPrompts);
}
/**
* 构造器调试
*
* @param experimentVo
* @return
*/
@PostMapping(value = "/experiment")
public Result<?> promptExperiment(@RequestBody AiragExperimentVo experimentVo, HttpServletRequest request) {
return airagPromptsService.promptExperiment(experimentVo,request);
}
/**
* 导出excel
*
* @param request
* @param airagPrompts
*/
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, AiragPrompts airagPrompts) {
return super.exportXls(request, airagPrompts, AiragPrompts.class, "airag_prompts");
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, AiragPrompts.class);
}
}

View File

@ -1,99 +0,0 @@
package org.jeecg.modules.airag.prompts.entity;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableLogic;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.jeecg.common.aspect.annotation.Dict;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @Description: airag_ext_data
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
@Data
@TableName("airag_ext_data")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="airag_ext_data")
public class AiragExtData implements Serializable {
private static final long serialVersionUID = 1L;
/**主键ID*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键ID")
private java.lang.String id;
/**业务类型标识(evaluator:评估器track:测试追踪)*/
@Excel(name = "业务类型标识(evaluator:评估器track:测试追踪)", width = 15)
@Schema(description = "业务类型标识(evaluator:评估器track:测试追踪)")
private java.lang.String bizType;
/**名称*/
@Excel(name = "名称", width = 15)
@Schema(description = "名称")
private java.lang.String name;
/**描述信息*/
@Excel(name = "描述信息", width = 15)
@Schema(description = "描述信息")
private java.lang.String descr;
/**标签,多个用逗号分隔*/
@Excel(name = "标签,多个用逗号分隔", width = 15)
@Schema(description = "标签,多个用逗号分隔")
private java.lang.String tags;
/**实际存储内容json*/
@Excel(name = "实际存储内容json", width = 15)
@Schema(description = "实际存储内容json")
private java.lang.String dataValue;
/**元数据,用于存储补充业务数据信息*/
@Excel(name = "元数据,用于存储补充业务数据信息", width = 15)
@Schema(description = "元数据,用于存储补充业务数据信息")
private java.lang.String metadata;
/**评测集数据*/
@Excel(name = "评测集数据", width = 15)
@Schema(description = "评测集数据")
private java.lang.String datasetValue;
/**创建人*/
@Schema(description = "创建人")
private java.lang.String createBy;
/**创建时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private java.util.Date createTime;
/**修改人*/
@Schema(description = "修改人")
private java.lang.String updateBy;
/**修改时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "修改时间")
private java.util.Date updateTime;
/**所属部门*/
@Schema(description = "所属部门")
private java.lang.String sysOrgCode;
/**租户id*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private java.lang.String tenantId;
/**状态*/
@Excel(name = "状态run:进行中 completed已完成", width = 15)
@Schema(description = "状态run:进行中 completed已完成")
private java.lang.String status;
/**版本*/
@Excel(name = "版本", width = 15)
@Schema(description = "版本")
private java.lang.Integer version;
}

View File

@ -1,108 +0,0 @@
package org.jeecg.modules.airag.prompts.entity;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableLogic;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.jeecg.common.aspect.annotation.Dict;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @Description: airag_prompts
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
@Data
@TableName("airag_prompts")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="airag_prompts")
public class AiragPrompts implements Serializable {
private static final long serialVersionUID = 1L;
/**主键ID*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键ID")
private java.lang.String id;
/**提示词名称*/
@Excel(name = "提示词名称", width = 15)
@Schema(description = "提示词名称")
private java.lang.String name;
/**提示词名称*/
@Excel(name = "提示key", width = 15)
@Schema(description = "提示key")
private java.lang.String promptKey;
/**提示词功能描述*/
@Excel(name = "提示词功能描述", width = 15)
@Schema(description = "提示词功能描述")
private java.lang.String description;
/**提示词模板内容,支持变量占位符如 {{variable}}*/
@Excel(name = "提示词模板内容,支持变量占位符如 {{variable}}", width = 15)
@Schema(description = "提示词模板内容,支持变量占位符如 {{variable}}")
private java.lang.String content;
/**提示词分类*/
@Excel(name = "提示词分类", width = 15)
@Schema(description = "提示词分类")
private java.lang.String category;
/**标签,多个逗号分割*/
@Excel(name = "标签,多个逗号分割", width = 15)
@Schema(description = "标签,多个逗号分割")
private java.lang.String tags;
/**适配的大模型ID*/
@Excel(name = "适配的大模型ID", width = 15)
@Schema(description = "适配的大模型ID")
private java.lang.String modelId;
/**大模型的参数配置*/
@Excel(name = "大模型的参数配置", width = 15)
@Schema(description = "大模型的参数配置")
private java.lang.String modelParam;
/**状态0:未发布 1:已发布)*/
@Excel(name = "状态0:未发布 1:已发布)", width = 15)
@Schema(description = "状态0:未发布 1:已发布)")
private java.lang.String status;
/**版本号(格式 0.0.1)*/
@Excel(name = "版本号(格式 0.0.1)", width = 15)
@Schema(description = "版本号(格式 0.0.1)")
private java.lang.String version;
/**删除状态0未删除 1已删除*/
@Excel(name = "删除状态0未删除 1已删除", width = 15)
@Schema(description = "删除状态0未删除 1已删除")
@TableLogic
private java.lang.Integer delFlag;
/**创建人*/
@Schema(description = "创建人")
private java.lang.String createBy;
/**创建日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建日期")
private java.util.Date createTime;
/**更新人*/
@Schema(description = "更新人")
private java.lang.String updateBy;
/**更新日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新日期")
private java.util.Date updateTime;
/**所属部门*/
@Schema(description = "所属部门")
private java.lang.String sysOrgCode;
/**租户id*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private java.lang.String tenantId;
}

View File

@ -1,17 +0,0 @@
package org.jeecg.modules.airag.prompts.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Description: airag_ext_data
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
public interface AiragExtDataMapper extends BaseMapper<AiragExtData> {
}

View File

@ -1,17 +0,0 @@
package org.jeecg.modules.airag.prompts.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Description: airag_prompts
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
public interface AiragPromptsMapper extends BaseMapper<AiragPrompts> {
}

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper">
</mapper>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper">
</mapper>

Some files were not shown because too many files have changed in this diff Show More