【合并升级v3.8.1】

Merge remote-tracking branch 'origin/master' into springboot3

# Conflicts:
#	jeecg-boot/README.md
#	jeecg-boot/db/tables_nacos.sql
#	jeecg-boot/jeecg-boot-base-core/pom.xml
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/encryption/AesEncryptUtil.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebMvcConfiguration.java
#	jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/pom.xml
#	jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/java/org/jeecg/modules/airag/app/controller/AiragAppController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/monitor/actuator/httptrace/CustomInMemoryHttpTraceRepository.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/service/impl/OpenApiPermissionServiceImpl.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysRoleIndexController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/ISysUserService.java
#	jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
#	jeecg-boot/jeecg-server-cloud/jeecg-system-cloud-start/src/main/java/org/jeecg/JeecgSystemCloudApplication.java
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel/pom.xml
#	jeecg-boot/pom.xml
#	jeecgboot-vue3/pnpm-lock.yaml
This commit is contained in:
JEECG
2025-07-08 16:33:51 +08:00
370 changed files with 12841 additions and 5591 deletions

View File

@ -2,12 +2,12 @@
JeecgBoot 低代码开发平台
===============
当前最新版本: 3.8.0发布日期2025-05-16
当前最新版本: 3.8.1发布日期2025-06-30
[![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/version-3.8.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.8.1-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 forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -35,7 +35,7 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart)
- QQ交流群 ⑩716488839、⑨808791225、其他(满)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
- 在线演示 [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取

File diff suppressed because one or more lines are too long

View File

@ -122,6 +122,11 @@
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
</dependency>
<!-- druid -->
<dependency>
@ -389,10 +394,5 @@
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter3-chatgpt</artifactId>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,6 +1,7 @@
package org.jeecg.common.api.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.jeecg.common.constant.CommonConstant;

View File

@ -9,14 +9,14 @@ import org.apache.commons.lang3.StringUtils;
public enum DySmsEnum {
/**登录短信模板编码*/
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**忘记密码短信模板编码*/
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**修改密码短信模板编码*/
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
/**注册账号短信模板编码*/
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**修改密码短信模板编码*/
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
/**注册账号短信模板编码*/
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
/**
* 短信模板编码
*/

View File

@ -3,6 +3,7 @@ package org.jeecg.common.exception;
import cn.hutool.core.util.ObjectUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import io.undertow.server.RequestTooBigException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.shiro.SecurityUtils;
@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.Map;
@ -56,7 +58,7 @@ public class JeecgBootExceptionHandler {
addSysLog(e);
return Result.error("校验失败!" + e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(",")));
}
/**
* 处理自定义异常
*/
@ -166,6 +168,27 @@ public class JeecgBootExceptionHandler {
return Result.error("文件大小超出10MB限制, 请压缩或降低文件质量! ");
}
/**
* 处理文件过大异常.
* jdk17中的MultipartException异常类已经被拆分成了MultipartException和MaxUploadSizeExceededException
* for [QQYUN-11716]上传大图片失败没有精确提示
* @param e
* @return
* @author chenrui
* @date 2025/4/8 16:13
*/
@ExceptionHandler(MultipartException.class)
public Result<?> handleMaxUploadSizeExceededException(MultipartException e) {
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException) {
log.error("文件大小超出限制: {}", cause.getMessage(), e);
addSysLog(e);
return Result.error("文件大小超出限制, 请压缩或降低文件质量!");
} else {
return handleException(e);
}
}
@ExceptionHandler(DataIntegrityViolationException.class)
public Result<?> handleDataIntegrityViolationException(DataIntegrityViolationException e) {
log.error(e.getMessage(), e);
@ -221,11 +244,16 @@ public class JeecgBootExceptionHandler {
} catch (NullPointerException | BeansException ignored) {
}
if (null != request) {
//update-begin---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
//请求的参数
Map<String, String[]> parameterMap = request.getParameterMap();
if(!CollectionUtils.isEmpty(parameterMap)){
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
if (!isTooBigException(e)) {
// 文件上传过大异常时不能获取参数,否则会报错
Map<String, String[]> parameterMap = request.getParameterMap();
if(!CollectionUtils.isEmpty(parameterMap)) {
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
}
}
//update-end---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
// 请求地址
log.setRequestUrl(request.getRequestURI());
//设置IP地址
@ -251,4 +279,26 @@ public class JeecgBootExceptionHandler {
}
//update-end---author:chenrui ---date:20240423 for[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
/**
* 是否文件过大异常
* for [QQYUN-11716]上传大图片失败没有精确提示
* @param e
* @return
* @author chenrui
* @date 2025/4/8 20:21
*/
private static boolean isTooBigException(Throwable e) {
boolean isTooBigException = false;
if(e instanceof MultipartException){
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException && cause.getCause() instanceof RequestTooBigException){
isTooBigException = true;
}
}
if(e instanceof MaxUploadSizeExceededException){
isTooBigException = true;
}
return isTooBigException;
}
}

View File

@ -2,7 +2,6 @@ package org.jeecg.common.system.base.entity;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
@ -10,6 +9,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

View File

@ -16,7 +16,13 @@ import java.util.regex.Pattern;
* @author zhoujf
*/
@Slf4j
public class SqlInjectionUtil {
public class SqlInjectionUtil {
/**
* sql注入黑名单数据库名
*/
public final static String XSS_STR_TABLE = "peformance_schema|information_schema";
/**
* 默认—sql注入关键词
*/
@ -167,7 +173,28 @@ public class SqlInjectionUtil {
}
return false;
}
/**
* 判断是否存在SQL注入关键词字符串
*
* @param keyword
* @return
*/
@SuppressWarnings("AlibabaUndefineMagicConstant")
private static boolean isExistSqlInjectTableKeyword(String sql, String keyword) {
// 需要匹配的sql注入关键词
String[] matchingTexts = new String[]{"`" + keyword, "(" + keyword, "(`" + keyword};
for (String matchingText : matchingTexts) {
String[] checkTexts = new String[]{" " + matchingText, "from" + matchingText};
for (String checkText : checkTexts) {
if (sql.contains(checkText)) {
return true;
}
}
}
return false;
}
/**
* sql注入过滤处理遇到注入关键字抛异常
*
@ -208,6 +235,14 @@ public class SqlInjectionUtil {
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
for (String xssTableStr : xssTableArr) {
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
// 三、SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
@ -244,6 +279,14 @@ public class SqlInjectionUtil {
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
for (String xssTableStr : xssTableArr) {
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
// 三、SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {

View File

@ -68,6 +68,13 @@ public class DbTypeUtils {
return dbTypeIf(dbType, DbType.ORACLE, DbType.ORACLE_12C, DbType.DM);
}
/**
* 是否是达梦
*/
public static boolean dbTypeIsDm(DbType dbType) {
return dbTypeIf(dbType, DbType.DM);
}
public static boolean dbTypeIsSqlServer(DbType dbType) {
return dbTypeIf(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
}

View File

@ -7,6 +7,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.BeanUtils;
import jakarta.servlet.http.HttpServletRequest;
@ -1133,5 +1134,14 @@ public class oConvertUtils {
public static <T> boolean isIn(T obj, T... objs) {
return isIn(obj, objs);
}
/**
* 判断租户ID是否有效
* @param tenantId
* @return
*/
public static boolean isEffectiveTenant(String tenantId) {
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
}
}

View File

@ -3,6 +3,8 @@ package org.jeecg.common.util.security;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.jeecg.common.util.oConvertUtils;
import java.util.*;
import java.util.regex.Matcher;
@ -66,6 +68,8 @@ public abstract class AbstractQueryBlackListHandler {
if(flag == false){
return false;
}
Set<String> xssTableSet = new HashSet<>(Arrays.asList(SqlInjectionUtil.XSS_STR_TABLE.split("\\|")));
for (QueryTable table : list) {
String name = table.getName();
String fieldRule = ruleMap.get(name);
@ -81,6 +85,16 @@ public abstract class AbstractQueryBlackListHandler {
}
}
// 判断是否调用了黑名单数据库
String dbName = table.getDbName();
if (oConvertUtils.isNotEmpty(dbName)) {
dbName = dbName.toLowerCase().trim();
if (xssTableSet.contains(dbName)) {
flag = false;
log.warn("sql黑名单校验数据库【" + dbName + "】禁止查询");
break;
}
}
}
// 返回黑名单校验结果(不合法直接抛出异常)
@ -135,6 +149,8 @@ public abstract class AbstractQueryBlackListHandler {
* 查询的表的信息
*/
protected class QueryTable {
//数据库名
private String dbName;
//表名
private String name;
//表的别名
@ -158,6 +174,14 @@ public abstract class AbstractQueryBlackListHandler {
this.fields.add(field);
}
public String getDbName() {
return dbName;
}
public void setDbName(String dbName) {
this.dbName = dbName;
}
public String getName() {
return name;
}

View File

@ -1,8 +1,9 @@
//package org.jeecg.config;
//
//
//import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
//import io.swagger.v3.oas.annotations.Operation;
//import org.jeecg.common.constant.CommonConstant;
//import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
//import org.springframework.beans.BeansException;
//import org.springframework.beans.factory.config.BeanPostProcessor;
//import org.springframework.context.annotation.Bean;
@ -18,15 +19,13 @@
//import springfox.documentation.builders.ParameterBuilder;
//import springfox.documentation.builders.PathSelectors;
//import springfox.documentation.builders.RequestHandlerSelectors;
//import springfox.documentation.oas.annotations.EnableOpenApi;
//import springfox.documentation.schema.ModelRef;
//import springfox.documentation.service.*;
//import springfox.documentation.spi.DocumentationType;
//import springfox.documentation.spi.service.contexts.SecurityContext;
//import springfox.documentation.spring.web.plugins.Docket;
//import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
//import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
//import springfox.documentation.swagger2.annotations.EnableSwagger2;
//import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
//
//import java.lang.reflect.Field;
//import java.util.ArrayList;
@ -38,8 +37,7 @@
// * @Author scott
// */
//@Configuration
//@EnableSwagger2 //开启 Swagger2
//@EnableKnife4j //开启 knife4j可以不写
//@EnableSwagger2WebMvc
//@Import(BeanValidatorPluginsConfiguration.class)
//public class Swagger2Config implements WebMvcConfigurer {
//
@ -97,6 +95,14 @@
// List<Parameter> pars = new ArrayList<>();
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tokenPar.build());
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
// ParameterBuilder tenantPar = new ParameterBuilder();
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tenantPar.build());
// }
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
//
// return pars;
// }
//
@ -151,7 +157,7 @@
//
// @Override
// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
// if (bean instanceof WebMvcRequestHandlerProvider) {
// customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
// }
// return bean;

View File

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

View File

@ -15,15 +15,15 @@ import jakarta.annotation.Resource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
import org.springframework.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@ -61,6 +61,14 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired(required = false)
private PrometheusMeterRegistry prometheusMeterRegistry;
/**
* meterRegistryPostProcessor
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
*/
@Autowired(required = false)
@Qualifier("meterRegistryPostProcessor")
private BeanPostProcessor meterRegistryPostProcessor;
/**
* 静态资源的配置 - 使得可以从磁盘中读取 Html、图片、视频、音频等
*/
@ -149,12 +157,17 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
/**
* 解决metrics端点不显示jvm信息的问题(zyf)
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
* @param event
* @author chenrui
* @date 2025/5/26 16:46
*/
@Bean
@ConditionalOnBean(name = "meterRegistryPostProcessor")
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
if(null != meterRegistryPostProcessor){
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
}
}
// /**

View File

@ -8,12 +8,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class LowCodeModeConfiguration implements WebMvcConfigurer {
public LowCodeModeInterceptor payInterceptor() {
return new LowCodeModeInterceptor();
private final LowCodeModeInterceptor lowCodeModeInterceptor;
public LowCodeModeConfiguration(LowCodeModeInterceptor lowCodeModeInterceptor) {
this.lowCodeModeInterceptor = lowCodeModeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(payInterceptor()).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
registry.addInterceptor(lowCodeModeInterceptor).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
}
}

View File

@ -12,6 +12,7 @@ import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.annotation.Resource;
@ -38,6 +39,7 @@ import java.util.Set;
* @date 20230904
*/
@Slf4j
@Component
public class LowCodeModeInterceptor implements HandlerInterceptor {
/**
* 低代码开发模式
@ -47,7 +49,8 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
@Autowired(required = false)
private CommonAPI commonAPI;
/**
@ -55,10 +58,15 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("低代码模式,拦截请求路径:" + request.getRequestURI());
//1、验证是否开启低代码开发模式控制
if (jeecgBaseConfig == null) {
jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
}
if (commonAPI == null) {
commonAPI = SpringContextUtils.getBean(CommonAPI.class);
}
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
String requestURI = request.getRequestURI().substring(request.getContextPath().length());

View File

@ -221,6 +221,7 @@ public class ShiroConfig {
registration.addUrlPatterns("/airag/flow/debug");
registration.addUrlPatterns("/airag/chat/send");
registration.addUrlPatterns("/airag/app/debug");
registration.addUrlPatterns("/airag/app/prompt/generate");
//支持异步
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
@ -320,7 +321,7 @@ public class ShiroConfig {
return sentinelManager;
}
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
RedisManager redisManager = new RedisManager();

View File

@ -1,2 +1,3 @@
springdoc.auto-tag-classes: false
springdoc.packages-to-scan: org.jeecg
springdoc.packages-to-scan: org.jeecg
springdoc.default-flat-param-object: true

View File

@ -53,19 +53,29 @@
<artifactId>jeecg-system-cloud-api</artifactId>
</dependency>-->
<!-- aiflow依赖 -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-aiflow</artifactId>
<version>1.0.5</version>
<version>1.1.1</version>
</dependency>
<!-- beigin 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
<version>${kotlin.version}</version>
<scope>provided</scope>
</dependency>
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-graaljs</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
<scope>provided</scope>
</dependency>
<!-- end 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>

View File

@ -16,6 +16,10 @@ public class AiAppConsts {
* 状态:禁用
*/
public static final String STATUS_DISABLE = "disable";
/**
* 状态:发布
*/
public static final String STATUS_RELEASE = "release";
/**

View File

@ -4,10 +4,13 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp;
@ -64,6 +67,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
* @return
*/
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:app:edit")
public Result<String> edit(@RequestBody AiragApp airagApp) {
AssertUtils.assertNotEmpty("参数异常", airagApp);
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
@ -73,6 +77,28 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
return Result.OK("保存完成!", airagApp.getId());
}
/**
* 发布应用
*
* @return
*/
@RequestMapping(value = "/release", method = RequestMethod.POST)
public Result<String> release(@RequestParam(name = "id") String id, @RequestParam(name = "release") Boolean release) {
AssertUtils.assertNotEmpty("id必须填写", id);
if (release == null) {
release = true;
}
AiragApp airagApp = new AiragApp();
airagApp.setId(id);
if (release) {
airagApp.setStatus(AiAppConsts.STATUS_RELEASE);
} else {
airagApp.setStatus(AiAppConsts.STATUS_ENABLE);
}
airagAppService.updateById(airagApp);
return Result.OK(release ? "发布成功" : "取消发布成功");
}
/**
* 通过id删除
*
@ -80,23 +106,23 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
* @return
*/
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
@RequiresPermissions("airag:app:delete")
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragApp app = airagAppService.getById(id);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == app || !app.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI应用失败不能删除其他租户的AI应用");
}
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
airagAppService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
this.airagAppService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*

View File

@ -1,16 +1,24 @@
package org.jeecg.modules.airag.app.controller;
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.constant.CommonConstant;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* airag应用-chat
*
@ -25,6 +33,15 @@ public class AiragChatController {
@Autowired
IAiragChatService chatService;
@Value(value = "${jeecg.path.upload}")
private String uploadpath;
/**
* 本地local miniominio 阿里alioss
*/
@Value(value="${jeecg.uploadType}")
private String uploadType;
/**
* 发送消息
@ -59,6 +76,19 @@ public class AiragChatController {
return chatService.send(chatSendParams);
}
/**
* 获取所有对话
*
* @return 返回一个Result对象包含所有对话的信息
* @author chenrui
* @date 2025/2/25 11:42
*/
@IgnoreAuth
@GetMapping(value = "/init")
public Result<?> initChat(@RequestParam(name = "id", required = true) String id) {
return chatService.initChat(id);
}
/**
* 获取所有对话
*
@ -141,4 +171,36 @@ public class AiragChatController {
return chatService.stop(requestId);
}
/**
* 上传文件
* for [QQYUN-12135]AI聊天上传图片提示非法token
*
* @param request
* @param response
* @return
* @throws Exception
* @author chenrui
* @date 2025/4/25 11:04
*/
@IgnoreAuth
@PostMapping(value = "/upload")
public Result<?> upload(HttpServletRequest request, HttpServletResponse response) throws Exception {
String bizPath = "airag";
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
// 获取上传文件对象
MultipartFile file = multipartRequest.getFile("file");
String savePath;
if (CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)) {
savePath = CommonUtils.uploadLocal(file, bizPath, uploadpath);
} else {
savePath = CommonUtils.upload(file, bizPath, uploadType);
}
Result<?> result = new Result<>();
result.setMessage(savePath);
result.setSuccess(true);
return result;
}
}

View File

@ -39,12 +39,13 @@ public class AiragApp implements Serializable {
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
private java.lang.String id;
/**
* 创建人
*/
@Schema(description = "创建人")
private String createBy;
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private java.lang.String createBy;
/**
* 创建日期
*/
@ -56,7 +57,7 @@ public class AiragApp implements Serializable {
* 更新人
*/
@Schema(description = "更新人")
private String updateBy;
private java.lang.String updateBy;
/**
* 更新日期
*/
@ -68,95 +69,95 @@ public class AiragApp implements Serializable {
* 所属部门
*/
@Schema(description = "所属部门")
private String sysOrgCode;
private java.lang.String sysOrgCode;
/**
* 租户id
*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private String tenantId;
private java.lang.String tenantId;
/**
* 应用名称
*/
@Excel(name = "应用名称", width = 15)
@Schema(description = "应用名称")
private String name;
private java.lang.String name;
/**
* 应用描述
*/
@Excel(name = "应用描述", width = 15)
@Schema(description = "应用描述")
private String descr;
private java.lang.String descr;
/**
* 应用图标
*/
@Excel(name = "应用图标", width = 15)
@Schema(description = "应用图标")
private String icon;
private java.lang.String icon;
/**
* 应用类型
*/
@Excel(name = "应用类型", width = 15, dicCode = "ai_app_type")
@Dict(dicCode = "ai_app_type")
@Schema(description = "应用类型")
private String type;
private java.lang.String type;
/**
* 开场白
*/
@Excel(name = "开场白", width = 15)
@Schema(description = "开场白")
private String prologue;
private java.lang.String prologue;
/**
* 预设问题
*/
@Excel(name = "预设问题", width = 15)
@Schema(description = "预设问题")
private String presetQuestion;
private java.lang.String presetQuestion;
/**
* 提示词
*/
@Excel(name = "提示词", width = 15)
@Schema(description = "提示词")
private String prompt;
private java.lang.String prompt;
/**
* 模型配置
*/
@Excel(name = "模型配置", width = 15, dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
@Schema(description = "模型配置")
private String modelId;
private java.lang.String modelId;
/**
* 历史消息数
*/
@Excel(name = "历史消息数", width = 15)
@Schema(description = "历史消息数")
private Integer msgNum;
private java.lang.Integer msgNum;
/**
* 知识库
*/
@Excel(name = "知识库", width = 15, dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
@Schema(description = "知识库")
private String knowledgeIds;
private java.lang.String knowledgeIds;
/**
* 流程
*/
@Excel(name = "流程", width = 15, dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
@Schema(description = "流程")
private String flowId;
private java.lang.String flowId;
/**
* 快捷指令
*/
@Excel(name = "快捷指令", width = 15)
@Schema(description = "快捷指令")
private String quickCommand;
private java.lang.String quickCommand;
/**
* 状态
* 状态enable=启用、disable=禁用、release=发布)
*/
@Excel(name = "状态", width = 15)
@Schema(description = "状态")
private String status;
private java.lang.String status;
/**
@ -164,7 +165,7 @@ public class AiragApp implements Serializable {
*/
@Excel(name = "元数据", width = 15)
@Schema(description = "元数据")
private String metadata;
private java.lang.String metadata;
/**
* 知识库ids

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.app.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.app.entity.AiragApp;
@ -11,4 +12,14 @@ import org.jeecg.modules.airag.app.entity.AiragApp;
*/
public interface AiragAppMapper extends BaseMapper<AiragApp> {
/**
* 根据ID查询app信息(忽略租户)
* @param id
* @return
* @author chenrui
* @date 2025/4/21 16:03
*/
@InterceptorIgnore(tenantLine = "true")
AiragApp getByIdIgnoreTenant(String id);
}

View File

@ -2,4 +2,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.app.mapper.AiragAppMapper">
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.app.entity.AiragApp">
SELECT * FROM airag_app WHERE id = #{id}
</select>
</mapper>

View File

@ -92,4 +92,14 @@ public interface IAiragChatService {
* @date 2025/3/3 19:49
*/
Result<?> clearMessage(String conversationId);
/**
* 初始化聊天(忽略租户)
* [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
* @param appId
* @return
* @author chenrui
* @date 2025/4/21 14:17
*/
Result<?> initChat(String appId);
}

View File

@ -13,6 +13,7 @@ import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.*;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragAppService;
import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.AppDebugParams;
@ -63,7 +64,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
RedisTemplate redisTemplate;
@Autowired
IAiragAppService airagAppService;
AiragAppMapper airagAppMapper;
@Autowired
IAiragFlowService airagFlowService;
@ -85,7 +86,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 获取app信息
AiragApp app = null;
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
app = airagAppService.getById(chatSendParams.getAppId());
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
}
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
// 更新标题
@ -146,13 +147,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
try {
// 发送完成事件
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
} catch (Exception e) {
log.error("终止会话时发生错误", e);
try {
// 防止异常冒泡
emitter.completeWithError(e);
} catch (Exception ignore) {}
} finally {
// 从缓存中移除emitter
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, eventData.getRequestId());
// 关闭emitter
emitter.complete();
try {
emitter.complete();
} catch (Exception ignore) {}
}
}
@ -237,6 +244,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
return Result.ok();
}
@Override
public Result<?> initChat(String appId) {
AiragApp app = airagAppMapper.getByIdIgnoreTenant(appId);
return Result.ok(app);
}
@Override
public Result<?> deleteConversation(String conversationId) {
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
@ -278,7 +291,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:27
*/
private String getConversationCacheKey(String conversationId,HttpServletRequest httpRequest) {
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
if (oConvertUtils.isEmpty(conversationId)) {
return null;
}
@ -376,7 +389,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:27
*/
private void saveChatConversation(ChatConversation chatConversation, boolean temp,HttpServletRequest httpRequest) {
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
if (null == chatConversation) {
return;
}
@ -414,8 +427,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
case AiragConsts.MESSAGE_ROLE_USER:
List<Content> contents = new ArrayList<>();
List<MessageHistory.ImageHistory> images = history.getImages();
if (oConvertUtils.isObjectNotEmpty(images)
&& !images.isEmpty()) {
if (oConvertUtils.isObjectNotEmpty(images) && !images.isEmpty()) {
contents.addAll(images.stream().map(imageHistory -> {
if (oConvertUtils.isNotEmpty(imageHistory.getUrl())) {
return ImageContent.from(imageHistory.getUrl());
@ -452,8 +464,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @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) {
if (message.type().equals(ChatMessageType.SYSTEM)) {
// 系统消息,放到消息列表最前面,并且不记录历史
@ -467,11 +478,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
histories = new ArrayList<>();
}
// 消息记录
MessageHistory historyMessage = MessageHistory.builder()
.conversationId(chatConversation.getId())
.topicId(topicId)
.datetime(DateUtils.now())
.build();
MessageHistory historyMessage = MessageHistory.builder().conversationId(chatConversation.getId()).topicId(topicId).datetime(DateUtils.now()).build();
if (message.type().equals(ChatMessageType.USER)) {
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_USER);
StringBuilder textContent = new StringBuilder();
@ -516,8 +523,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 每次会话都生成一个新的,用来缓存emitter
String requestId = UUIDGenerator.generate();
SseEmitter emitter = new SseEmitter(-0L);
emitter.onError(throwable -> {
log.warn("SEE向客户端发送消息失败: {}", throwable.getMessage());
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, requestId);
try {
emitter.complete();
} catch (Exception ignore) {}
});
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
eventRequestId.setData(EventMessageData.builder().message("").build());
sendMessage2Client(emitter, eventRequestId);
// 缓存emitter
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE, requestId, emitter);
// 缓存开始发送时间
log.info("[AI-CHAT]开始发送消息,requestId:{}", requestId);
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId, System.currentTimeMillis());
try {
// 组装用户消息
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
@ -589,36 +609,51 @@ public class AiragChatServiceImpl implements IAiragChatService {
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
flowRunParams.setEventCallback(eventData -> {
if (EventData.EVENT_FLOW_FINISHED.equals(eventData.getEvent())) {
// 打印耗时日志
printChatDuration(requestId, "流程执行完毕");
// 已经执行完了,删除时间缓存
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
EventFlowData data = (EventFlowData) eventData.getData();
Object outputs = data.getOutputs();
if (oConvertUtils.isObjectNotEmpty(outputs)) {
AiMessage aiMessage;
if (outputs instanceof String) {
// 兼容推理模型
String messageText = String.valueOf(outputs);
messageText = messageText.replaceAll("<think>([\\s\\S]*?)</think>", "> $1");
aiMessage = new AiMessage(messageText);
} else {
aiMessage = new AiMessage(JSONObject.toJSONString(outputs));
if(data.isSuccess()) {
Object outputs = data.getOutputs();
if (oConvertUtils.isObjectNotEmpty(outputs)) {
AiMessage aiMessage;
if (outputs instanceof String) {
// 兼容推理模型
String messageText = String.valueOf(outputs);
messageText = messageText.replaceAll("<think>([\\s\\S]*?)</think>", "> $1");
aiMessage = new AiMessage(messageText);
} else {
aiMessage = new AiMessage(JSONObject.toJSONString(outputs));
}
EventData msgEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message(aiMessage.text()).build();
msgEventData.setData(messageEventData);
msgEventData.setRequestId(requestId);
sendMessage2Client(emitter, msgEventData);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
}
EventData msgEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder()
.message(aiMessage.text())
.build();
msgEventData.setData(messageEventData);
try {
String eventStr = JSONObject.toJSONString(msgEventData);
log.debug("[AI应用]接收FLOW返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
throw new RuntimeException(e);
}else{
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
// 失败
String message = data.getMessage();
if (message != null && message.contains(FlowConsts.FLOW_ERROR_MSG_LLM_TIMEOUT)) {
message = "当前用户较多,排队中,请稍后再试!";
EventData errEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
errEventData.setData(EventMessageData.builder().message("\n" + message).build());
sendMessage2Client(emitter, errEventData);
errEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
// 如果是超时,主动关闭SSE,防止流程切面中返回异常消息导致前端不能正常展示上面的{普通消息}.
closeSSE(emitter, errEventData);
}
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
}
}
});
// 打印流程耗时日志
printChatDuration(requestId, "开始执行流程");
airagFlowService.runFlow(flowRunParams);
}
@ -649,24 +684,26 @@ public class AiragChatServiceImpl implements IAiragChatService {
String metadataStr = aiApp.getMetadata();
if (oConvertUtils.isNotEmpty(metadataStr)) {
JSONObject metadata = JSONObject.parseObject(metadataStr);
if(oConvertUtils.isNotEmpty(metadata)){
if (oConvertUtils.isNotEmpty(metadata)) {
if (metadata.containsKey("temperature")) {
aiChatParams.setTemperature(metadata.getDouble("temperature"));
}
if (metadata.containsKey("topP")) {
aiChatParams.setTopP(metadata.getDouble("temperature"));
aiChatParams.setTopP(metadata.getDouble("topP"));
}
if (metadata.containsKey("presencePenalty")) {
aiChatParams.setPresencePenalty(metadata.getDouble("temperature"));
aiChatParams.setPresencePenalty(metadata.getDouble("presencePenalty"));
}
if (metadata.containsKey("frequencyPenalty")) {
aiChatParams.setFrequencyPenalty(metadata.getDouble("temperature"));
aiChatParams.setFrequencyPenalty(metadata.getDouble("frequencyPenalty"));
}
if (metadata.containsKey("maxTokens")) {
aiChatParams.setMaxTokens(metadata.getInteger("temperature"));
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
}
}
}
// 打印流程耗时日志
printChatDuration(requestId, "构造应用自定义参数完成");
// 发消息
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
}
@ -683,10 +720,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:24
*/
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId,
List<ChatMessage> messages,AIChatParams aiChatParams) {
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
// 调用ai聊天
if(null == aiChatParams){
if (null == aiChatParams) {
aiChatParams = new AIChatParams();
}
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
@ -694,13 +730,15 @@ public class AiragChatServiceImpl implements IAiragChatService {
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
TokenStream chatStream;
try {
// 打印流程耗时日志
printChatDuration(requestId, "开始向LLM发送消息");
if (oConvertUtils.isNotEmpty(modelId)) {
chatStream = aiChatHandler.chat(modelId, messages, aiChatParams);
} else {
chatStream = aiChatHandler.chatByDefaultModel(messages, aiChatParams);
}
} catch (Exception e) {
log.error(e.getMessage(),e);
log.error(e.getMessage(), e);
throw new JeecgBootBizTipException("调用大模型接口失败:" + e.getMessage());
}
/**
@ -709,91 +747,120 @@ public class AiragChatServiceImpl implements IAiragChatService {
AtomicBoolean isThinking = new AtomicBoolean(false);
// ai聊天响应逻辑
chatStream.onNext((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, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder()
.message(resMessage)
.build();
eventData.setData(messageEventData);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
try {
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.onComplete((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.content();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
try {
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
throw new RuntimeException(e);
}
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation,false,httpRequest);
closeSSE(emitter, eventData);
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
// 需要执行工具
// TODO author: chenrui for: date:2025/3/7
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
})
.onError((Throwable error) -> {
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
})
.start();
// 兼容推理模型
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, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message(resMessage).build();
eventData.setData(messageEventData);
eventData.setRequestId(requestId);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
sendMessage2Client(emitter, eventData);
}).onComplete((responseMessage) -> {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息完成");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// 记录ai的回复
AiMessage aiMessage = responseMessage.content();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
closeSSE(emitter, eventData);
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
// 需要执行工具
// TODO author: chenrui for: date:2025/3/7
} else if (FinishReason.LENGTH.equals(finishReason)) {
// 上下文长度超过限制
log.error("调用模型异常:上下文长度超过限制:{}", responseMessage.tokenUsage());
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
eventData.setData(EventMessageData.builder().message("\n上下文长度超过限制请调整模型最大Tokens").build());
sendMessage2Client(emitter, eventData);
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
closeSSE(emitter, eventData);
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
}).onError((Throwable error) -> {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息异常");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭{}", requestId);
return;
}
log.error(error.getMessage(), error);
String errMsg = error.getMessage();
if (errMsg != null && errMsg.contains("timeout")) {
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
errMsg = "当前用户较多,排队中,请稍后再试!";
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
eventData.setData(EventMessageData.builder().message("\n" + errMsg).build());
sendMessage2Client(emitter, eventData);
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
closeSSE(emitter, eventData);
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
} else {
errMsg = "调用大模型接口失败:" + errMsg;
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
}
}).start();
}
/**
* 发送消息到客户端
*
* @param emitter
* @param eventData
* @author chenrui
* @date 2025/4/22 19:58
*/
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
try {
log.info("发送消息:{}", eventData.getRequestId());
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
log.error("发送消息失败", e);
}
}
/**
@ -837,12 +904,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
CompletableFuture.runAsync(() -> {
List<ChatMessage> messages = new LinkedList<>();
String systemMsgStr = "根据用户的问题,总结会话标题.\n" +
"要求如下:\n" +
"1. 使用中文回答.\n" +
"2. 标题长度控制在5个汉字10个英文字符以内\n" +
"3. 直接回复会话标题,不要有其他任何无关描述\n" +
"4. 如果无法总结,回复不知道\n";
String systemMsgStr = "根据用户的问题,总结会话标题.\n" + "要求如下:\n" + "1. 使用中文回答.\n" + "2. 标题长度控制在5个汉字10个英文字符以内\n" + "3. 直接回复会话标题,不要有其他任何无关描述\n" + "4. 如果无法总结,回复不知道\n";
messages.add(new SystemMessage(systemMsgStr));
messages.add(new UserMessage(question));
String summaryTitle;
@ -876,6 +938,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
/**
* 获取用户名
*
* @param httpRequest
* @return
* @author chenrui
@ -885,9 +948,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
try {
TokenUtils.getTokenByRequest();
String token;
if(null != httpRequest){
if (null != httpRequest) {
token = TokenUtils.getTokenByRequest(httpRequest);
}else{
} else {
token = TokenUtils.getTokenByRequest();
}
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) {
@ -898,4 +961,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
return null;
}
/**
* 打印耗时
* @param requestId
* @param message
* @author chenrui
* @date 2025/4/28 15:15
*/
private static void printChatDuration(String requestId,String message) {
Long beginTime = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
if (null != beginTime) {
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
}
}
}

View File

@ -0,0 +1,83 @@
package org.jeecg.modules.airag.demo;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
import org.jeecgframework.poi.excel.ExcelImportUtil;
import org.jeecgframework.poi.excel.entity.ImportParams;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Java增强Demo: Excel数据读取器
* for [QQYUN-11718]【AI】积木报表对接AI流程编排接口展示报表
* @Author: chenrui
* @Date: 2025/4/29 16:51
*/
@Component("jimuDataReader")
@Slf4j
public class JimuDataReader implements IAiRagEnhanceJava {
@Override
public Map<String, Object> process(Map<String, Object> inputParams) {
// inputParams: {"bizData":"/xxxx/xxxx/xxxx/xxxx.xls"}
try {
String filePath = (String) inputParams.get("bizData");
if (filePath == null || filePath.isEmpty()) {
throw new IllegalArgumentException("File path is empty");
}
File excelFile = new File(filePath);
if (!excelFile.exists() || !excelFile.isFile()) {
throw new IllegalArgumentException("File not found: " + filePath);
}
// Since we don't know the target entity class, we'll read the Excel generically
return readExcelData(excelFile);
} catch (Exception e) {
log.error("Error processing Excel file", e);
throw new JeecgBootBizTipException("调用java增强失败", e);
}
}
/**
* Excel导入工具方法基于ExcelImportUtil
*
* @param file Excel文件
* @return Excel读取结果包含字段和数据
* @throws Exception 导入过程中的异常
*/
public static Map<String, Object> readExcelData(File file) throws Exception {
Map<String, Object> result = new HashMap<>();
// 设置导入参数
ImportParams params = new ImportParams();
params.setTitleRows(0); // 没有标题
params.setHeadRows(1); // 第一行是表头
// 读取Excel数据
List<Map<String, Object>> dataList = ExcelImportUtil.importExcel(file, Map.class, params);
// 如果没有数据,返回空结果
if (dataList == null || dataList.isEmpty()) {
result.put("fields", new ArrayList<>());
result.put("datas", new ArrayList<>());
return result;
}
// 从第一行数据中获取字段名
List<String> fieldNames = new ArrayList<>(dataList.get(0).keySet());
result.put("fields", fieldNames);
result.put("datas", dataList);
return result;
}
}

View File

@ -0,0 +1,22 @@
package org.jeecg.modules.airag.demo;
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Map;
/**
* @Description: Java增强节点示例类
* @Author: chenrui
* @Date: 2025/3/6 11:42
*/
@Component("testAiragEnhance")
public class TestAiragEnhance implements IAiRagEnhanceJava {
@Override
public Map<String, Object> process(Map<String, Object> inputParams) {
Object arg1 = inputParams.get("arg1");
Object arg2 = inputParams.get("arg2");
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
}
}

View File

@ -35,6 +35,11 @@ public class LLMConsts {
*/
public static final String MODEL_TYPE_LLM = "LLM";
/**
* 向量模型:默认维度
*/
public static final Integer EMBED_MODEL_DEFAULT_DIMENSION = 1536;
/**
* 知识库:文档状态:草稿
*/
@ -47,7 +52,10 @@ public class LLMConsts {
* 知识库:文档状态:构建完成
*/
public static final String KNOWLEDGE_DOC_STATUS_COMPLETE = "complete";
/**
* 知识库:文档状态:构建失败
*/
public static final String KNOWLEDGE_DOC_STATUS_FAILED = "failed";
/**
* 知识库:文档类型:文本

View File

@ -4,9 +4,12 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
@ -74,6 +77,7 @@ public class AiragKnowledgeController {
* @date 2025/2/18 17:09
*/
@PostMapping(value = "/add")
@RequiresPermissions("airag:knowledge:add")
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
airagKnowledgeService.save(airagKnowledge);
@ -90,6 +94,7 @@ public class AiragKnowledgeController {
*/
@Transactional(rollbackFor = Exception.class)
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:knowledge:edit")
public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) {
AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId());
if (airagKnowledgeEntity == null) {
@ -113,6 +118,7 @@ public class AiragKnowledgeController {
* @date 2025/3/12 17:05
*/
@PutMapping(value = "/rebuild")
@RequiresPermissions("airag:knowledge:rebuild")
public Result<?> rebuild(@RequestParam("knowIds") String knowIds) {
String[] knowIdArr = knowIds.split(",");
for (String knowId : knowIdArr) {
@ -131,29 +137,24 @@ public class AiragKnowledgeController {
*/
@Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
@RequiresPermissions("airag:knowledge:delete")
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragKnowledge know = airagKnowledgeService.getById(id);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == know || !know.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI知识库失败不能删除其他租户的AI知识库");
}
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
airagKnowledgeDocService.removeByKnowIds(Collections.singletonList(id));
airagKnowledgeService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除知识库
*
* @param ids
* @return
* @author chenrui
* @date 2025/2/18 17:09
*/
@Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
List<String> idsList = Arrays.asList(ids.split(","));
airagKnowledgeDocService.removeByKnowIds(idsList);
airagKnowledgeService.removeByIds(idsList);
return Result.OK("批量删除成功!");
}
/**
* 通过id查询知识库
*
@ -203,6 +204,7 @@ public class AiragKnowledgeController {
* @date 2025/2/18 15:47
*/
@PostMapping(value = "/doc/edit")
@RequiresPermissions("airag:knowledge:doc:edit")
public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) {
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
}
@ -215,6 +217,7 @@ public class AiragKnowledgeController {
* @date 2025/3/20 11:29
*/
@PostMapping(value = "/doc/import/zip")
@RequiresPermissions("airag:knowledge:doc:zip")
public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId,
@RequestParam(name = "file", required = true) MultipartFile file) {
return airagKnowledgeDocService.importDocumentFromZip(knowId,file);
@ -241,6 +244,7 @@ public class AiragKnowledgeController {
* @date 2025/2/18 15:47
*/
@PutMapping(value = "/doc/rebuild")
@RequiresPermissions("airag:knowledge:doc:rebuild")
public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) {
return airagKnowledgeDocService.rebuildDocument(docIds);
}
@ -255,12 +259,49 @@ public class AiragKnowledgeController {
*/
@Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/doc/deleteBatch")
public Result<String> deleteDocumentBatch(@RequestParam(name = "ids", required = true) String ids) {
@RequiresPermissions("airag:knowledge:doc:deleteBatch")
public Result<String> deleteDocumentBatch(HttpServletRequest request, @RequestParam(name = "ids", required = true) String ids) {
List<String> idsList = Arrays.asList(ids.split(","));
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
List<AiragKnowledgeDoc> docList = airagKnowledgeDocService.listByIds(idsList);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
docList.forEach(airagKnowledgeDoc -> {
if (null == airagKnowledgeDoc || !airagKnowledgeDoc.getTenantId().equals(currentTenantId)) {
throw new IllegalArgumentException("删除AI知识库文档失败不能删除其他租户的AI知识库文档");
}
});
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
airagKnowledgeDocService.removeDocByIds(idsList);
return Result.OK("批量删除成功!");
}
/**
* 清空知识库文档
*
* @param
* @return
*/
@Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/doc/deleteAll")
@RequiresPermissions("airag:knowledge:doc:deleteAll")
public Result<?> deleteDocumentAll(HttpServletRequest request, @RequestParam(name = "knowId") String knowId) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragKnowledge know = airagKnowledgeService.getById(knowId);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == know || !know.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI知识库失败不能删除其他租户的AI知识库");
}
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
return airagKnowledgeDocService.deleteAllByKnowId(knowId);
}
/**
* 命中测试

View File

@ -3,12 +3,23 @@ package org.jeecg.modules.airag.llm.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 dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.embedding.EmbeddingModel;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.ai.factory.AiModelFactory;
import org.jeecg.ai.factory.AiModelOptions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
import org.jeecg.modules.airag.llm.handler.EmbeddingHandler;
import org.jeecg.modules.airag.llm.service.IAiragModelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@ -17,6 +28,7 @@ import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Collections;
/**
* @Description: AiRag模型配置
@ -32,6 +44,8 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
@Autowired
private IAiragModelService airagModelService;
@Autowired
AIChatHandler aiChatHandler;
/**
* 分页列表查询
@ -57,7 +71,12 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
* @return
*/
@PostMapping(value = "/add")
@RequiresPermissions("airag:model:add")
public Result<String> add(@RequestBody AiragModel airagModel) {
// 验证 模型名称/模型类型/基础模型
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType());
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
airagModelService.save(airagModel);
return Result.OK("添加成功!");
}
@ -69,6 +88,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
* @return
*/
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:model:edit")
public Result<String> edit(@RequestBody AiragModel airagModel) {
airagModelService.updateById(airagModel);
return Result.OK("编辑成功!");
@ -81,23 +101,23 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
* @return
*/
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
@RequiresPermissions("airag:model:delete")
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragModel model = airagModelService.getById(id);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == model || !model.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI模型失败不能删除其他租户的AI模型");
}
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
airagModelService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
this.airagModelService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
@ -136,4 +156,25 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
return super.importExcel(request, response, AiragModel.class);
}
@PostMapping(value = "/test")
public Result<?> test(@RequestBody AiragModel airagModel) {
// 验证 模型名称/模型类型/基础模型
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType());
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
try {
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("test connection")), null);
}else{
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
embeddingModel.embed("test text");
}
}catch (Exception e){
log.error("测试模型连接失败", e);
return Result.error("测试模型连接失败" + e.getMessage());
}
return Result.OK("测试模型连接成功");
}
}

View File

@ -30,13 +30,14 @@ public class AiragKnowledge implements Serializable {
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
private java.lang.String id;
/**
* 创建人
*/
@Schema(description = "创建人")
private String createBy;
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private java.lang.String createBy;
/**
* 创建日期
@ -50,7 +51,7 @@ public class AiragKnowledge implements Serializable {
* 更新人
*/
@Schema(description = "更新人")
private String updateBy;
private java.lang.String updateBy;
/**
* 更新日期
@ -64,21 +65,21 @@ public class AiragKnowledge implements Serializable {
* 所属部门
*/
@Schema(description = "所属部门")
private String sysOrgCode;
private java.lang.String sysOrgCode;
/**
* 租户id
*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private String tenantId;
private java.lang.String tenantId;
/**
* 知识库名称
*/
@Excel(name = "知识库名称", width = 15)
@Schema(description = "知识库名称")
private String name;
private java.lang.String name;
/**
* 向量模型id
@ -86,19 +87,19 @@ public class AiragKnowledge implements Serializable {
@Excel(name = "向量模型id", width = 15, dictTable = "airag_model where model_type = 'EMBED'", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_model where model_type = 'EMBED'", dicText = "name", dicCode = "id")
@Schema(description = "向量模型id")
private String embedId;
private java.lang.String embedId;
/**
* 描述
*/
@Excel(name = "描述", width = 15)
@Schema(description = "描述")
private String descr;
private java.lang.String descr;
/**
* 状态
*/
@Excel(name = "状态", width = 15)
@Schema(description = "状态")
private String status;
private java.lang.String status;
}

View File

@ -3,6 +3,7 @@ package org.jeecg.modules.airag.llm.entity;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.*;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
@ -40,6 +41,7 @@ public class AiragKnowledgeDoc implements Serializable {
* 创建人
*/
@Schema(description = "创建人")
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private String createBy;
/**
@ -119,9 +121,4 @@ public class AiragKnowledgeDoc implements Serializable {
@Schema(description = "状态")
private String status;
/**
* 服务器基础路径
*/
@TableField(exist = false)
private String baseUrl;
}

View File

@ -45,6 +45,7 @@ public class AiragModel implements Serializable {
* 创建人
*/
@Schema(description = "创建人")
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private String createBy;
/**
* 创建日期

View File

@ -12,7 +12,7 @@ import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.service.IAiragModelService;
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@ -38,7 +38,7 @@ import java.util.regex.Matcher;
public class AIChatHandler implements IAIChatHandler {
@Autowired
IAiragModelService airagModelService;
AiragModelMapper airagModelMapper;
@Autowired
EmbeddingHandler embeddingHandler;
@ -82,7 +82,7 @@ public class AIChatHandler implements IAIChatHandler {
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
AssertUtils.assertNotEmpty("请选择模型", modelId);
AiragModel airagModel = airagModelService.getById(modelId);
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
return completions(airagModel, messages, params);
}
@ -96,7 +96,7 @@ public class AIChatHandler implements IAIChatHandler {
* @author chenrui
* @date 2025/2/24 17:30
*/
private String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
public String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
params = mergeParams(airagModel, params);
String resp = llmHandler.completions(messages, params);
if (resp.contains("</think>")
@ -150,7 +150,7 @@ public class AIChatHandler implements IAIChatHandler {
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
AssertUtils.assertNotEmpty("请选择模型", modelId);
AiragModel airagModel = airagModelService.getById(modelId);
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
return chat(airagModel, messages, params);
}
@ -226,7 +226,7 @@ public class AIChatHandler implements IAIChatHandler {
params.setMaxTokens(modelParams.getInteger("maxTokens"));
}
if (oConvertUtils.isObjectEmpty(params.getTimeout())) {
params.setMaxTokens(modelParams.getInteger("timeout"));
params.setTimeout(modelParams.getInteger("timeout"));
}
}
@ -237,6 +237,16 @@ public class AIChatHandler implements IAIChatHandler {
params.setQueryRouter(queryRouter);
}
// 设置确保maxTokens值正确
if (oConvertUtils.isObjectNotEmpty(params.getMaxTokens()) && params.getMaxTokens() <= 0) {
params.setMaxTokens(null);
}
// 默认超时时间
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
params.setTimeout(60);
}
return params;
}

View File

@ -35,8 +35,9 @@ import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.jeecg.modules.airag.llm.service.IAiragModelService;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -69,13 +70,15 @@ public class EmbeddingHandler implements IEmbeddingHandler {
EmbedStoreConfigBean embedStoreConfigBean;
@Autowired
@Lazy
private IAiragModelService airagModelService;
private AiragModelMapper airagModelMapper;
@Autowired
@Lazy
private IAiragKnowledgeService airagKnowledgeService;
@Autowired
private AiragKnowledgeMapper airagKnowledgeMapper;
@Value(value = "${jeecg.path.upload:}")
private String uploadpath;
@ -112,6 +115,13 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/
private static final ConcurrentHashMap<String, EmbeddingStore<TextSegment>> EMBED_STORE_CACHE = new ConcurrentHashMap<>();
/**
* 正则匹配: md图片
* "!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)"
*/
private static final Pattern PATTERN_MD_IMAGE = Pattern.compile("!\\[(.*?)]\\((.*?)\\)");
/**
* 向量化文档
*
@ -183,6 +193,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
* @author chenrui
* @date 2025/2/18 16:52
*/
@Override
public KnowledgeSearchResult embeddingSearch(List<String> knowIds, String queryText, Integer topNumber, Double similarity) {
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
@ -223,7 +234,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/
public List<Map<String, Object>> searchEmbedding(String knowId, String queryText, Integer topNumber, Double similarity) {
AssertUtils.assertNotEmpty("请选择知识库", knowId);
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
AiragKnowledge knowledge = airagKnowledgeMapper.getByIdIgnoreTenant(knowId);
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
@ -268,6 +279,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
* @author chenrui
* @date 2025/2/20 21:03
*/
@Override
public QueryRouter getQueryRouter(List<String> knowIds, Integer topNumber, Double similarity) {
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
List<ContentRetriever> retrievers = Lists.newArrayList();
@ -275,7 +287,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
if (oConvertUtils.isEmpty(knowId)) {
continue;
}
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
AiragKnowledge knowledge = airagKnowledgeMapper.getByIdIgnoreTenant(knowId);
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
AiModelOptions modelOptions = buildModelOptions(model);
@ -345,7 +357,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/
private AiragModel getEmbedModelData(String modelId) {
AssertUtils.assertNotEmpty("向量模型不能为空", modelId);
AiragModel model = airagModelService.getById(modelId);
AiragModel model = airagModelMapper.getByIdIgnoreTenant(modelId);
AssertUtils.assertNotEmpty("向量模型不存在", model);
AssertUtils.assertEquals("仅支持向量模型", LLMConsts.MODEL_TYPE_EMBED, model.getModelType());
return model;
@ -371,6 +383,18 @@ public class EmbeddingHandler implements IEmbeddingHandler {
AiModelOptions modelOp = buildModelOptions(model);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
String tableName = embedStoreConfigBean.getTable();
// update-begin---author:sunjianlei ---date:20250509 for【QQYUN-12345】向量模型维度不一致问题
// 如果该模型不是默认的向量维度
int dimension = embeddingModel.dimension();
if (!LLMConsts.EMBED_MODEL_DEFAULT_DIMENSION.equals(dimension)) {
// 就加上维度后缀,防止因维度不一致导致保存失败
tableName += ("_" + dimension);
}
// update-end-----author:sunjianlei ---date:20250509 for【QQYUN-12345】向量模型维度不一致问题
EmbeddingStore<TextSegment> embeddingStore = PgVectorEmbeddingStore.builder()
// Connection and table parameters
.host(embedStoreConfigBean.getHost())
@ -378,7 +402,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
.database(embedStoreConfigBean.getDatabase())
.user(embedStoreConfigBean.getUser())
.password(embedStoreConfigBean.getPassword())
.table(embedStoreConfigBean.getTable())
.table(tableName)
// Embedding dimension
// Required: Must match the embedding models output dimension
.dimension(embeddingModel.dimension())
@ -449,16 +473,20 @@ public class EmbeddingHandler implements IEmbeddingHandler {
String fileType = FilenameUtils.getExtension(docFile.getName());
if ("md".contains(fileType)) {
// 如果是md文件查找所有图片语法如果是本地图片替换成网络图片
String baseUrl = doc.getBaseUrl() + "/sys/common/static/";
String baseUrl = "#{domainURL}/sys/common/static/";
String sourcePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH);
if(oConvertUtils.isNotEmpty(sourcePath)) {
String escapedPath = uploadpath;
if (File.separator.equals("\\")){
//update-begin---author:wangshuai---date:2025-06-03---for:【QQYUN-12636】【AI知识库】文档库上传 本地local 文档中的图片不展示---
/*if (File.separator.equals("\\")){
escapedPath = uploadpath.replace("//", "\\\\");
}
}*/
//update-end---author:wangshuai---date:2025-06-03---for:【QQYUN-12636】【AI知识库】文档库上传 本地local 文档中的图片不展示---
sourcePath = sourcePath.replaceFirst("^" + escapedPath, "").replace("\\", "/");
baseUrl = baseUrl + sourcePath + "/";
StringBuffer sb = replaceImageUrl(content, baseUrl);
String docFilePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
docFilePath = FilenameUtils.getPath(docFilePath);
docFilePath = docFilePath.replace("\\", "/");
StringBuffer sb = replaceImageUrl(content, baseUrl + sourcePath + "/", baseUrl + docFilePath);
content = sb.toString();
}
}
@ -469,11 +497,9 @@ public class EmbeddingHandler implements IEmbeddingHandler {
}
@NotNull
private static StringBuffer replaceImageUrl(String content, String baseUrl) {
private static StringBuffer replaceImageUrl(String content, String abstractBaseUrl, String relativeBaseUrl) {
// 正则表达式匹配md文件中的图片语法 ![alt text](image url)
String mdImagePattern = "!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)";
Pattern pattern = Pattern.compile(mdImagePattern);
Matcher matcher = pattern.matcher(content);
Matcher matcher = PATTERN_MD_IMAGE.matcher(content);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
@ -481,7 +507,16 @@ public class EmbeddingHandler implements IEmbeddingHandler {
// 检查是否是本地图片路径
if (!imageUrl.startsWith("http")) {
// 替换成网络图片路径
String networkImageUrl = baseUrl + imageUrl;
String networkImageUrl = abstractBaseUrl + imageUrl;
if(imageUrl.startsWith("/")) {
// 绝对路径
networkImageUrl = abstractBaseUrl + imageUrl;
}else{
// 相对路径
networkImageUrl = relativeBaseUrl + imageUrl;
}
// 修改图片路径中//->/但保留http://和https://
networkImageUrl = networkImageUrl.replaceAll("(?<!http:)(?<!https:)//", "/");
matcher.appendReplacement(sb, "![" + matcher.group(1) + "](" + networkImageUrl + ")");
} else {
matcher.appendReplacement(sb, "![" + matcher.group(1) + "](" + imageUrl + ")");

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.llm.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
@ -11,4 +12,14 @@ import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
*/
public interface AiragKnowledgeMapper extends BaseMapper<AiragKnowledge> {
/**
* 根据ID查询知识库信息(忽略租户)
* for [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
* @param id
* @return
* @author chenrui
* @date 2025/4/21 15:24
*/
@InterceptorIgnore(tenantLine = "true")
AiragKnowledge getByIdIgnoreTenant(String id);
}

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.llm.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.llm.entity.AiragModel;
@ -11,4 +12,14 @@ import org.jeecg.modules.airag.llm.entity.AiragModel;
*/
public interface AiragModelMapper extends BaseMapper<AiragModel> {
/**
* 根据ID查询模型信息(忽略租户)
* for [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
* @param id
* @return
* @author chenrui
* @date 2025/4/21 15:24
*/
@InterceptorIgnore(tenantLine = "true")
AiragModel getByIdIgnoreTenant(String id);
}

View File

@ -2,4 +2,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.llm.mapper.AiragKnowledgeMapper">
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.llm.entity.AiragKnowledge">
SELECT * FROM airag_knowledge WHERE id = #{id}
</select>
</mapper>

View File

@ -2,4 +2,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.llm.mapper.AiragModelMapper">
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.llm.entity.AiragModel">
SELECT * FROM airag_model WHERE id = #{id}
</select>
</mapper>

View File

@ -67,6 +67,14 @@ public interface IAiragKnowledgeDocService extends IService<AiragKnowledgeDoc> {
*/
Result<?> removeDocByIds(List<String> docIds);
/**
* 通过知识库id删除所以文档
*
* @param knowId
* @return
*/
Result<?> deleteAllByKnowId(String knowId);
/**
* 从zip包导入文档
* @param knowId

View File

@ -1,9 +1,12 @@
package org.jeecg.modules.airag.llm.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.io.FilenameUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.config.TenantContext;
@ -26,9 +29,12 @@ import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
@ -36,8 +42,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import static org.jeecg.modules.airag.llm.consts.LLMConsts.*;
@ -79,6 +83,12 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
*/
private static final ExecutorService buildDocExecutorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
// 解压文件:单个文件最大150MB
private static final long MAX_FILE_SIZE = 150 * 1024 * 1024;
// 解压文件:总解压大小1024MB
private static final long MAX_TOTAL_SIZE = 1024 * 1024 * 1024;
// 解压文件:最多解压10000个Entry
private static final int MAX_ENTRY_COUNT = 10000;
@Transactional(rollbackFor = {Exception.class})
@Override
@ -122,7 +132,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
AssertUtils.assertNotEmpty("文档不存在", docList);
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String baseUrl = CommonUtils.getBaseUrl(request);
// 检查状态
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
.filter(doc -> {
@ -143,7 +152,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
})
.peek(doc -> {
doc.setStatus(KNOWLEDGE_DOC_STATUS_BUILDING);
doc.setBaseUrl(baseUrl);
})
.collect(Collectors.toList());
if (oConvertUtils.isObjectEmpty(knowledgeDocs)) {
@ -174,13 +182,11 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
this.updateById(doc);
log.info("重建文档成功, 知识库id: {}, 文档id: {}", knowId, doc.getId());
} else {
doc.setStatus(KNOWLEDGE_DOC_STATUS_DRAFT);
this.updateById(doc);
this.handleDocBuildFailed(doc, "向量化失败");
log.info("重建文档失败, 知识库id: {}, 文档id: {}", knowId, doc.getId());
}
}catch (Throwable t){
doc.setStatus(KNOWLEDGE_DOC_STATUS_DRAFT);
this.updateById(doc);
this.handleDocBuildFailed(doc, t.getMessage());
log.error("重建文档失败:" + t.getMessage() + ", 知识库id: " + knowId + ", 文档id: " + doc.getId(), t);
}
//update-end---author:chenrui ---date:20250410 for[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
@ -190,6 +196,24 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
return Result.ok("操作成功");
}
/**
* 处理文档构建失败
*/
private void handleDocBuildFailed(AiragKnowledgeDoc doc, String failedReason) {
doc.setStatus(KNOWLEDGE_DOC_STATUS_FAILED);
String metadataStr = doc.getMetadata();
JSONObject metadata;
if (oConvertUtils.isEmpty(metadataStr)) {
metadata = new JSONObject();
} else {
metadata = JSONObject.parseObject(metadataStr);
}
metadata.put("failedReason", failedReason);
doc.setMetadata(metadata.toJSONString());
this.updateById(doc);
}
@Override
public Result<?> removeByKnowIds(List<String> knowIds) {
@ -198,8 +222,16 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
AiragKnowledge airagKnowledge = airagKnowledgeMapper.selectById(knowId);
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
// 异步删除向量数据
final String embedId = airagKnowledge.getEmbedId();
final String finalKnowId = knowId;
CompletableFuture.runAsync(() -> {
try {
embeddingHandler.deleteEmbedDocsByKnowId(finalKnowId, embedId);
} catch (Throwable ignore) {
}
});
// 删除数据
embeddingHandler.deleteEmbedDocsByKnowId(knowId, airagKnowledge.getEmbedId());
airagKnowledgeDocMapper.deleteByMainId(knowId);
}
return Result.OK();
@ -223,13 +255,39 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
AiragKnowledge airagKnowledge = airagKnowledgeMapper.selectById(knowId);
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
// 异步删除向量数据
final String embedId = airagKnowledge.getEmbedId();
final List<String> docIdsToDelete = new ArrayList<>(groupedDocIds);
CompletableFuture.runAsync(() -> {
try {
embeddingHandler.deleteEmbedDocsByDocIds(docIdsToDelete, embedId);
} catch (Throwable ignore) {
}
});
// 删除数据
embeddingHandler.deleteEmbedDocsByDocIds(groupedDocIds, airagKnowledge.getEmbedId());
airagKnowledgeDocMapper.deleteBatchIds(groupedDocIds);
});
return Result.ok("success");
}
@Override
public Result<?> deleteAllByKnowId(String knowId) {
if (oConvertUtils.isEmpty(knowId)) {
return Result.error("知识库id不能为空");
}
LambdaQueryWrapper<AiragKnowledgeDoc> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AiragKnowledgeDoc::getKnowledgeId, knowId);
//noinspection unchecked
wrapper.select(AiragKnowledgeDoc::getId);
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectList(wrapper);
if (docList.isEmpty()) {
return Result.ok("暂无文档");
}
List<String> docIds = docList.stream().map(AiragKnowledgeDoc::getId).collect(Collectors.toList());
this.removeDocByIds(docIds);
return Result.ok("清空完成");
}
@Transactional(rollbackFor = {java.lang.Exception.class})
@Override
public Result<?> importDocumentFromZip(String knowId, MultipartFile zipFile) {
@ -283,6 +341,7 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
doc.setMetadata(metadata.toJSONString());
docList.add(doc);
});
AssertUtils.assertNotEmpty("压缩包中没有符合要求的文档", docList);
// 保存数据
this.saveBatch(docList);
// 重建文档
@ -299,57 +358,113 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
/**
* 解压缩文件
*
* @param zipFilePath
* @param destDir
* @param afterExtract
* @param zipFilePath 压缩文件路径
* @param destDir 目标文件夹
* @param afterExtract 解压完成后回调
* @throws IOException
* @author chenrui
* @date 2025/3/20 14:37
*/
public static void unzipFile(String zipFilePath, String destDir, Consumer<File> afterExtract) throws
IOException {
// 创建目标目录
File dir = new File(destDir);
if (!dir.exists()) {
dir.mkdirs();
public static void unzipFile(String zipFilePath, String destDir, Consumer<File> afterExtract) throws IOException {
unzipFile(Paths.get(zipFilePath), Paths.get(destDir), afterExtract);
}
/**
* 解压缩文件
*
* @param zipFilePath 压缩文件路径
* @param targetDir 目标文件夹
* @param afterExtract 解压完成后回调
* @throws IOException
* @author chenrui
* @date 2025/4/28 17:02
*/
private static void unzipFile(Path zipFilePath, Path targetDir, Consumer<File> afterExtract) throws IOException {
long totalUnzippedSize = 0;
int entryCount = 0;
if (!Files.exists(targetDir)) {
Files.createDirectories(targetDir);
}
try (ZipFile zipFile = new ZipFile(zipFilePath)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
byte[] buffer = new byte[1024];
try (ZipFile zipFile = new ZipFile(zipFilePath.toFile())) {
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
ZipEntry ze = entries.nextElement();
File newFile = new File(destDir, ze.getName());
// 预防 ZIP 路径穿越攻击
String canonicalDestDirPath = dir.getCanonicalPath();
String canonicalFilePath = newFile.getCanonicalPath();
if (!canonicalFilePath.startsWith(canonicalDestDirPath + File.separator)) {
throw new IOException("ZIP 路径穿越攻击被阻止: " + ze.getName());
ZipArchiveEntry entry = entries.nextElement();
entryCount++;
if (entryCount > MAX_ENTRY_COUNT) {
throw new IOException("解压文件数量超限可能是zip bomb攻击");
}
if (ze.isDirectory()) {
newFile.mkdirs();
} else {
// 创建父目录
new File(newFile.getParent()).mkdirs();
Path newPath = safeResolve(targetDir, entry.getName());
// 读取 ZIP 文件并写入新文件
try (InputStream zis = zipFile.getInputStream(ze);
FileOutputStream fos = new FileOutputStream(newFile)) {
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
if (entry.isDirectory()) {
Files.createDirectories(newPath);
} else {
Files.createDirectories(newPath.getParent());
try (InputStream is = zipFile.getInputStream(entry);
OutputStream os = Files.newOutputStream(newPath)) {
long bytesCopied = copyLimited(is, os, MAX_FILE_SIZE);
totalUnzippedSize += bytesCopied;
if (totalUnzippedSize > MAX_TOTAL_SIZE) {
throw new IOException("解压总大小超限可能是zip bomb攻击");
}
}
// 解压完成后回调
if (afterExtract != null) {
afterExtract.accept(newFile);
afterExtract.accept(newPath.toFile());
}
}
}
}
}
/**
* 安全解析路径防止Zip Slip攻击
*
* @param targetDir
* @param entryName
* @return
* @throws IOException
* @author chenrui
* @date 2025/4/28 16:46
*/
private static Path safeResolve(Path targetDir, String entryName) throws IOException {
Path resolvedPath = targetDir.resolve(entryName).normalize();
if (!resolvedPath.startsWith(targetDir)) {
throw new IOException("ZIP 路径穿越攻击被阻止:" + entryName);
}
return resolvedPath;
}
/**
* 复制输入流到输出流,并限制最大字节数
*
* @param in
* @param out
* @param maxBytes
* @return
* @throws IOException
* @author chenrui
* @date 2025/4/28 17:03
*/
private static long copyLimited(InputStream in, OutputStream out, long maxBytes) throws IOException {
byte[] buffer = new byte[8192];
long totalCopied = 0;
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
totalCopied += bytesRead;
if (totalCopied > maxBytes) {
throw new IOException("单个文件解压超限可能是zip bomb攻击");
}
out.write(buffer, 0, bytesRead);
}
return totalCopied;
}
}

View File

@ -1,33 +0,0 @@
package org.jeecg.modules.airag.llm.vo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* 知识库查询返回结果
*
* @Author: chenrui
* @Date: 2025/2/18 17:53
*/
@Data
@NoArgsConstructor
public class KnowledgeSearchResult {
/**
* 命中的文档内容
*/
String data;
/**
* 命中的文档列表
*/
List<Map<String, Object>> documents;
public KnowledgeSearchResult(String data, List<Map<String, Object>> documents) {
this.data = data;
this.documents = documents;
}
}

View File

@ -0,0 +1,94 @@
package org.jeecg.modules.airag.ocr.controller;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.commons.collections.CollectionUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.modules.airag.ocr.entity.AiOcr;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/airag/ocr")
public class AiOcrController {
@Autowired
private RedisUtil redisUtil;
private static final String AI_OCR_REDIS_KEY = "airag:ocr";
@GetMapping("/list")
public Result<?> list(){
Object aiOcr = redisUtil.get(AI_OCR_REDIS_KEY);
IPage<AiOcr> page = new Page<>(1,10);
if(null != aiOcr){
List<AiOcr> aiOcrList = JSONObject.parseArray(aiOcr.toString(), AiOcr.class);
page.setRecords(aiOcrList);
page.setTotal(aiOcrList.size());
page.setPages(aiOcrList.size());
}
return Result.OK(page);
}
@PostMapping("/add")
public Result<String> add(@RequestBody AiOcr aiOcr){
Object aiOcrList = redisUtil.get(AI_OCR_REDIS_KEY);
aiOcr.setId(UUID.randomUUID().toString().replace("-",""));
if(null == aiOcrList){
List<AiOcr> list = new ArrayList<>();
list.add(aiOcr);
redisUtil.set(AI_OCR_REDIS_KEY, JSONObject.toJSONString(list));
}else{
List<AiOcr> aiOcrs = JSONObject.parseArray(aiOcrList.toString(), AiOcr.class);
aiOcrs.add(aiOcr);
redisUtil.set(AI_OCR_REDIS_KEY,JSONObject.toJSONString(aiOcrs));
}
return Result.OK("添加成功");
}
@PutMapping("/edit")
public Result<String> updateById(@RequestBody AiOcr aiOcr){
Object aiOcrList = redisUtil.get(AI_OCR_REDIS_KEY);
if(null != aiOcrList){
List<AiOcr> aiOcrs = JSONObject.parseArray(aiOcrList.toString(), AiOcr.class);
aiOcrs.forEach(item->{
if(item.getId().equals(aiOcr.getId())){
BeanUtils.copyProperties(aiOcr,item);
}
});
redisUtil.set(AI_OCR_REDIS_KEY,JSONObject.toJSONString(aiOcrs));
}else{
return Result.OK("编辑失败,未找到该数据");
}
return Result.OK("编辑成功");
}
@DeleteMapping("/deleteById")
public Result<String> deleteById(@RequestBody AiOcr aiOcr){
Object aiOcrObj = redisUtil.get(AI_OCR_REDIS_KEY);
if(null != aiOcrObj){
List<AiOcr> aiOcrs = JSONObject.parseArray(aiOcrObj.toString(), AiOcr.class);
List<AiOcr> aiOcrList = new ArrayList<>();
for(AiOcr ocr: aiOcrs){
if(!ocr.getId().equals(aiOcr.getId())){
aiOcrList.add(ocr);
}
}
if(CollectionUtils.isNotEmpty(aiOcrList)){
redisUtil.set(AI_OCR_REDIS_KEY,JSONObject.toJSONString(aiOcrList));
}else{
redisUtil.removeAll(AI_OCR_REDIS_KEY);
}
}else{
return Result.OK("删除失败,未找到该数据");
}
return Result.OK("删除成功");
}
}

View File

@ -0,0 +1,29 @@
package org.jeecg.modules.airag.ocr.entity;
import lombok.Data;
/**
* @Description: OCR识别实体类
*
* @author: wangshuai
* @date: 2025/4/16 17:01
*/
@Data
public class AiOcr {
/**
* 编号
*/
private String id;
/**
* 标题
*/
private String title;
/**
* 提示词
*/
private String prompt;
}

View File

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 -->
<property name="LOG_HOME" value="../logs" />
<!--<property name="COLOR_PATTERN" value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta( %replace(%caller{1}){'\t|Caller.{1}0|\r\n', ''})- %gray(%msg%xEx%n)" />-->
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度%msg日志消息%n是换行符
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/jeecgboot-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度%msg日志消息%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>
</encoder>
</appender>
<!-- 生成 error html格式日志开始 -->
<appender name="HTML" class="ch.qos.logback.core.FileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!--设置日志级别,过滤掉info日志,只输入error日志-->
<level>ERROR</level>
</filter>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<pattern>%p%d%msg%M%F{32}%L</pattern>
</layout>
</encoder>
<file>${LOG_HOME}/error-log.html</file>
</appender>
<!-- 生成 error html格式日志结束 -->
<!-- 每天生成一个html格式的日志开始 -->
<appender name="FILE_HTML" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/jeecgboot-%d{yyyy-MM-dd}.%i.html</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<pattern>%p%d%msg%M%F{32}%L</pattern>
</layout>
</encoder>
</appender>
<!-- 每天生成一个html格式的日志结束 -->
<!--myibatis log configure -->
<logger name="com.apache.ibatis" level="TRACE" />
<logger name="java.sql.Connection" level="DEBUG" />
<logger name="java.sql.Statement" level="DEBUG" />
<logger name="java.sql.PreparedStatement" level="DEBUG" />
<logger name="logging.level.dev.langchain4j" level="DEBUG" />
<logger name="logging.level.dev.ai4j.openai4j" level="DEBUG" />
<!-- 日志输出级别 -->
<root level="info">
<appender-ref ref="STDOUT" />
<!-- <appender-ref ref="FILE" />-->
<!-- <appender-ref ref="HTML" />-->
<!-- <appender-ref ref="FILE_HTML" />-->
</root>
</configuration>

View File

@ -1,7 +1,10 @@
//package org.jeecg.modules.airag.test;
//
//import dev.langchain4j.agent.tool.ToolParameters;
//import dev.langchain4j.agent.tool.ToolSpecification;
//import dev.langchain4j.data.message.*;
//import dev.langchain4j.model.chat.ChatLanguageModel;
//import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
//import dev.langchain4j.model.openai.OpenAiChatModel;
//import dev.langchain4j.model.output.Response;
//import dev.langchain4j.service.AiServices;
@ -12,15 +15,13 @@
//import org.jeecg.ai.factory.AiModelOptions;
//import org.jeecg.ai.handler.AIParams;
//import org.jeecg.ai.handler.LLMHandler;
//import org.jeecg.modules.airag.llm.handler.AIChatParams;
//import org.junit.Test;
//import org.junit.jupiter.api.Test;
//
//import java.io.IOException;
//import java.io.Serial;
//import java.nio.file.Files;
//import java.nio.file.Paths;
//import java.util.ArrayList;
//import java.util.Base64;
//import java.util.Collections;
//import java.util.*;
//import java.util.concurrent.ConcurrentHashMap;
//import java.util.concurrent.CountDownLatch;
//
@ -174,4 +175,51 @@
// }
//
//
// @Test
// public void testFuncCalling() {
// String apiKey = "sk-";
// String baseUrl = "https://api.v3.cm/v1";
// String modelName = "gpt-4o-mini";
// double temperature = 0.7;
// ChatLanguageModel llmModel = OpenAiChatModel.builder()
// .apiKey(apiKey)
// .baseUrl(baseUrl)
// .modelName(modelName)
// .temperature(temperature)
// .build();
//
// List<ToolSpecification> toolSpecifications = new ArrayList<>();
// Map<String, Map<String, Object>> properties = new ConcurrentHashMap<>();
//
// properties.put("location", new HashMap<>() {
// @Serial
// private static final long serialVersionUID = -8440944665582258534L;
// {
// put("type", "string");
// put("description", "The city and state, e.g. San Francisco, CA");
// }
// });
// ToolSpecification toolSpecification = ToolSpecification.builder()
// .name("get_current_weather")
// .description("Get the current weather in a given location")
// .parameters(ToolParameters.builder()
// .properties(properties)
// .required(List.of("location"))
// .type("string")
// .build())
// .build();
// toolSpecifications.add(toolSpecification);
//
// List<ChatMessage> messages = new ArrayList<>();
// messages.add(UserMessage.from("How is the weather in Beijing today?"));
//
// Response<AiMessage> resp = llmModel.generate(messages, toolSpecifications);
// System.out.println(resp);
//
// /*Response { content = AiMessage { text = null
// toolExecutionRequests = [ToolExecutionRequest { id = "call_hjPaRh9WAHv6Ib7KpgHpp8Tl", name = "get_current_weather", arguments = "{"location":"Beijing, China"}" }] },
// tokenUsage = TokenUsage { inputTokenCount = 69, outputTokenCount = 19, totalTokenCount = 88 }, finishReason = TOOL_EXECUTION, metadata = {} }*/
// }
//
//
//}

View File

@ -1,97 +0,0 @@
常见问题
1.pnpm安装依赖报错
错误: node\_modules\\vite\\node\_modules\\esbuild\\esbuild.exe ENOENT
解决方案https://blog.csdn.net/weixin_41760500/article/details/119885574命令node ./node_modules/esbuild/install.js
2.pnpm安装后访问提示缺少依赖

解决方案: https://stackoverflow.com/questions/70597494/pnpm-does-not-resolve-dependencies
3.pnpm install出现错误
错误ERR\_PNPM\_PEER\_DEP\_ISSUES Unmet peer dependencies
http://ms521.cn/index.php/Home/Index/article/aid/271
4.项目安装依赖无问题,访问页面报错
前端部分报错:
[plugin:vite:vue-jsx] Cannot find package 'C:\Users\123\Desktop\JeecgBoot-master\pincone_system\jeecgboot-vue3\node_modules\.pnpm\@vitejs+plugin-vue-jsx@3.1.0_vite@5.4.9_@types+node@20.16.13_less@4.2.0_terser@5.36.0__vue@3.5.12_typescript@4.9.5_\node_modules\@babel\plugin-transform-typescript\lib\index.js' imported from C:\Users\123\Desktop\JeecgBoot-master\pincone_system\jeecgboot-vue3\node_modules\.pnpm\@vitejs+plugin-vue-jsx@3.1.0_vite@5.4.9_@types+node@20.16.13_less@4.2.0_terser@5.36.0__vue@3.5.12_typescript@4.9.5_\node_modules\@vitejs\plugin-vue-jsx\dist\index.cjs Did you mean to import "@babel/plugin-transform-typescript/lib/index.js"? #7396
回答: 是因为项目的路径太长导致 相关问题Issues查看相关博客
• https://blog.csdn.net/weixin_43235500/article/details/142144989
• https://blog.csdn.net/qq_25996219/article/details/140328092
5. 通过npm install启动报错
建议请使用pnpm i 可以避免更多问题
错误情况:

解决:进入提示的路径 \node_modules\vite-plugin-mock\node_modules\esbuild\
执行命令: node install.js再启动就好了
6. 前端刷新进不了登录页面
报错props.ts:15 Uncaught (in promise) SyntaxError: Unexpected token '='
错误截图:
原因:谷歌浏览器版本过低,升级浏览器
比如这边版本就过低了
7.表单如何全部禁用
加上这个属性就可以了

效果

8.table列表如何自定义排序
defSort: {
column: 'id',
order: 'desc',
},
参考示例:
9.idea编写js时爆红提示statement expected
https://blog.csdn.net/mlsama/article/details/80633009
10.抽屉的setDrawerProps不好使值会还原
11. 如何删除不需要的demo制作一个精简版本
精简项目删除demo等非必须功能
12.vue3 暗黑模式下显示不完整
错误示例:
解决方案:
在样式中的字体颜色和背景颜色使用@变量名称来代替
color: @text-color;
background-color: @component-background;
[info] 通用样式变量名称可以在在目录bulid->vite->plugin->themes.ts中找到,darkModifyVars是重写antd中的样式
[info]更多样式变量名称请参考目录node_modules/es/style/themes/default.less

改造完成之后的效果
13.操作列“删除按钮”界面布局异常
相关issue
https://github.com/jeecgboot/jeecgboot-vue3/issues/458
问题截图:
解决方案找到popConfirm填写属性placement: 'left'
placement: 'left',
效果截图
14.在centos7中下载依赖pnpm i时 mozjpeg依赖下载不下来
https://github.com/jeecgboot/jeecgboot-vue3/issues/433#issuecomment-1510470534
15.日期遮挡问题
问题截图:
下拉显示组件,页面滚动时,页面出现错位遮挡问题
解决方案:将组件挂载到父节点上
getPopupContainer: (node) => node.parentNode,
效果截图
16.nextTick作用
在 Vue 3 中nextTick 方法用于在 DOM 更新之后执行回调函数。它的作用是在下次 DOM 更新循环结束后执行一些操作,以确保你在操作更新的 DOM 元素时能够获取到最新的结果。nextTick 方法可以用于以下情况:
1 在更新数据后立即操作 DOM 元素。
2 在更新组件后执行某些逻辑或触发一些副作用。
3 在更新后获取更新后的 DOM 元素的尺寸或位置等信息。
使用nextTick 方法有两种方式1.使用回调函数:
nextTick(()=>
//在DOM更新后执行的操作
}):
2.使用Promise:
nextTick().then(()=>
//在DOM更新后执行的操作
})
无论使用哪种方式传入的回调函数或Promisel回调都会在下一次DOM更新周期之后被调用。这样可以确保在数据变化后Vue已经完成了相应的DOM更新。
需要注意的是nextTick 方法是异步执行的因此不能保证回调函数会立即执行。如果需要等待nextTick执行完成可以使用await关键字或者. then()方法来等待Promise的完成。
17. 一个页面多个表格,列的展示会互相影响
[info] 相关issue
https://github.com/jeecgboot/jeecgboot-vue3/issues/1064
[info]问题截图
[info] 解决方案在不同的tableProps下设置不同的checkKey即可解决
tableSetting: { cacheKey: 'depart_user_departInfo' },
18. 通过npm install启动报错

可以使用这个命令:
npm install --ignore-scripts

View File

@ -1,8 +1,8 @@
//package org.jeecg.modules.demo.cloud.controller;
//
//import com.alibaba.csp.sentinel.annotation.SentinelResource;
//import io.swagger.annotations.Api;
//import io.swagger.annotations.ApiOperation;
//import io.swagger.v3.oas.annotations.tags.Tag;
//import io.swagger.v3.oas.annotations.Operation;
//import lombok.extern.slf4j.Slf4j;
//import org.jeecg.common.api.vo.Result;
//import org.jeecg.common.system.api.ISysBaseAPI;
@ -18,7 +18,7 @@
// *
// */
//@Slf4j
//@Api(tags = "【微服务】单元测试")
//@Tag(name = "【微服务】单元测试")
//@RestController
//@RequestMapping("/test")
//public class JcloudDemoFeignController {
@ -34,7 +34,7 @@
// */
// @GetMapping("/callSystem")
// //@SentinelResource(value = "remoteDict",fallback = "getDefaultHandler")
// @ApiOperation(value = "通过feign调用system服务", notes = "测试jeecg-demo服务是否通过fegin调用system服务接口")
// @Operation(summary = "通过feign调用system服务")
// public Result getRemoteDict() {
// List<DictModel> list = sysBaseApi.queryAllDict();
// return Result.OK(list);
@ -48,7 +48,7 @@
//// * @return
//// */
//// @GetMapping("/callErp")
//// @ApiOperation(value = "测试feign erp", notes = "测试feign erp")
//// @Operation(summary = "测试feign erp")
//// public Result callErp() {
//// log.info("call erp 服务");
//// String res = erpHelloApi.callHello();

View File

@ -1,95 +0,0 @@
package org.jeecg.modules.demo.gpt.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.chatgpt.service.AiChatService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
//update-begin---author:chenrui ---date:20240110 for[QQYUN-5509]AI生成表结构和软文------------
/**
* @Description: chatGpt接口
* @Author: chenrui
* @Date: 2024/1/9 16:30
*/
@Tag(name = "AI接口")
@RestController
@RequestMapping("/test/ai")
@Slf4j
public class AiController {
private static final String CACHE_PREFIX = "ai:resp:";
@Autowired
AiChatService aiChatService;
/**
* 通过AI生成模块表设计
* @param descr 描述
* @return
* @author chenrui
* @date 2024/1/9 20:12
*/
@AutoLog(value = "通过AI生成模块表设计")
@PostMapping(value = "/gen/schema/modules")
@Operation(summary = "通过AI生成模块表设计")
public Result<String> genSchemaModules(@RequestParam(name = "prompt", required = true) String prompt) {
String result = aiChatService.genSchemaModules(prompt);
return Result.ok(result);
}
/**
* 通过AI生成软文
* @param descr 描述
* @return
* @author chenrui
* @date 2024/1/9 20:12
*/
@AutoLog(value = "通过AI生成软文")
@PostMapping(value = "/gen/article")
@Operation(summary = "通过AI生成软文")
public Result<String> genArticle(@RequestParam(name = "prompt", required = true) String prompt) {
String result = aiChatService.genArticleWithMd(prompt);
return Result.ok(result);
}
/**
* 向AI提问
* @param message
* @return
* @author chenrui
* @date 2024/1/15 19:11
*/
@AutoLog(value = "向AI提问")
@PostMapping(value = "/completions")
@Operation(summary = "向AI提问")
public Result<?> completions(@RequestParam(name = "message", required = true) String message) {
String result = aiChatService.completions(message);
return Result.ok(result);
}
/**
* 让AI生成图片
* @param prompt
* @return
* @author chenrui
* @date 2024/1/15 19:11
*/
@AutoLog(value = "让AI生成图片")
@PostMapping(value = "/gen/image")
@Operation(summary = "让AI生成图片")
public Result<?> genImage(@RequestParam(name = "prompt", required = true) String prompt) {
String result = aiChatService.imageGenerate(prompt);
return Result.ok(result);
}
}
//update-end---author:chenrui ---date:20240110 for//update-begin---author:chenrui ---date:20240110 for[QQYUN-5509]AI生成表结构和软文------------------------

View File

@ -4,8 +4,6 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
@ -64,7 +62,7 @@ public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoSe
* @param req
* @return
*/
@Operation(summary = "获取所有Demo数据列表")
@Operation(summary = "获取Demo数据列表")
@GetMapping(value = "/list")
@PermissionData(pageComponent = "jeecg/JeecgDemoList")
public Result<?> list(JeecgDemo jeecgDemo, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
@ -477,7 +475,6 @@ public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoSe
* 测试Mono对象
* @return
*/
@Operation(summary = "Mono测试")
@GetMapping(value ="/test")
public Mono<String> test() {
//解决shiro报错No SecurityManager accessible to the calling code, either bound to the org.apache.shiro

View File

@ -1,7 +1,7 @@
package org.jeecg.modules.demo.test.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;

View File

@ -3,7 +3,6 @@ package org.jeecg.modules.demo.test.entity;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.Version;
import io.swagger.v3.oas.annotations.media.Schema;
import org.jeecg.common.system.base.entity.JeecgEntity;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
@ -11,6 +10,8 @@ import org.springframework.format.annotation.DateTimeFormat;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

View File

@ -24,6 +24,12 @@
<artifactId>hibernate-re</artifactId>
</dependency>
<!-- AI大模型管理 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-module-airag</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- 企业微信/钉钉 api -->
<dependency>
<groupId>org.jeecgframework</groupId>
@ -40,10 +46,16 @@
</exclusion>
</exclusions>
</dependency>
<!-- mongo、redis和文件数据集支持包按需引入 -->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-nosql-starter</artifactId>
</dependency>
<!-- 后台导出接口Echart图表支持包按需引入
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-echarts-starter</artifactId>
</dependency> -->
<!-- 积木BI -->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>

View File

@ -27,6 +27,10 @@ import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Description: 邮箱发送信息
@ -54,32 +58,37 @@ public class EmailSendMsgHandle implements ISendMsgHandle {
* 真实姓名变量
*/
private static final String realNameExp = "{REALNAME}";
/**
* 线程池用于异步发送消息
*/
public static ExecutorService cachedThreadPool = new ThreadPoolExecutor(0, 1024, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
@Override
public void sendMsg(String esReceiver, String esTitle, String esContent) {
JavaMailSender mailSender = (JavaMailSender) SpringContextUtils.getBean("mailSender");
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = null;
//update-begin-authortaoyan date:20200811 for:配置类数据获取
if(oConvertUtils.isEmpty(emailFrom)){
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
setEmailFrom(staticConfig.getEmailFrom());
}
//update-end-authortaoyan date:20200811 for:配置类数据获取
try {
helper = new MimeMessageHelper(message, true);
// 设置发送方邮箱地址
helper.setFrom(emailFrom);
helper.setTo(esReceiver);
helper.setSubject(esTitle);
helper.setText(esContent, true);
mailSender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
cachedThreadPool.execute(()->{
try {
log.info("============> 开始邮件发送,接收人:"+esReceiver);
MimeMessageHelper helper = new MimeMessageHelper(message, true);
// 设置发送方邮箱地址
helper.setFrom(emailFrom);
helper.setTo(esReceiver);
helper.setSubject(esTitle);
helper.setText(esContent, true);
mailSender.send(message);
log.info("============> 邮件发送成功,接收人:"+esReceiver);
} catch (MessagingException e) {
log.error("============> 邮件发送失败,接收人:"+esReceiver, e.getMessage());
}
});
}
@Override
@ -180,24 +189,26 @@ public class EmailSendMsgHandle implements ISendMsgHandle {
private void sendEmail(String email, String content, String title){
JavaMailSender mailSender = (JavaMailSender) SpringContextUtils.getBean("mailSender");
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = null;
if (oConvertUtils.isEmpty(emailFrom)) {
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
setEmailFrom(staticConfig.getEmailFrom());
}
try {
helper = new MimeMessageHelper(message, true);
// 设置发送方邮箱地址
helper.setFrom(emailFrom);
helper.setTo(email);
//设置抄送人
helper.setCc(email);
helper.setSubject(title);
helper.setText(content, true);
mailSender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
cachedThreadPool.execute(()->{
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true);
// 设置发送方邮箱地址
helper.setFrom(emailFrom);
helper.setTo(email);
//设置抄送人
helper.setCc(email);
helper.setSubject(title);
helper.setText(content, true);
mailSender.send(message);
log.info("============> 邮件发送成功,接收人:"+email);
} catch (MessagingException e) {
log.warn("============> 邮件发送失败,接收人:"+email, e.getMessage());
}
});
}
//update-end-author:taoyan date:2023-6-20 for: QQYUN-5557【简流】通知节点 发送邮箱 表单上有一个邮箱字段,流程中,邮件发送节点,邮件接收人 不可选择邮箱

View File

@ -19,6 +19,21 @@ public class CustomInMemoryHttpTraceRepository extends InMemoryHttpExchangeRepos
return super.findAll();
}
/**
* for [issues/8309]系统监控>请求追踪,列表每刷新一下,总数据就减一#8309
* @param trace
* @author chenrui
* @date 2025/6/4 19:38
*/
@Override
public void add(HttpExchange trace) {
// 只有当请求不是OPTIONS方法并且URI不包含httptrace时才记录数据
if (!"OPTIONS".equals(trace.getRequest().getMethod()) &&
!trace.getRequest().getUri().toString().contains("httptrace")) {
super.add(trace);
}
}
public List<HttpExchange> findAll(String query) {
List<HttpExchange> allTrace = super.findAll();
if (null != allTrace && !allTrace.isEmpty()) {

View File

@ -162,7 +162,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
String method = openApi.getRequestMethod();
String appkey = request.getHeader("appkey");
OpenApiAuth openApiAuth = openApiAuthService.getByAppkey(appkey);
SysUser systemUser = sysUserService.getById(openApiAuth.getSystemUserId());
SysUser systemUser = sysUserService.getUserByName(openApiAuth.getCreateBy());
String token = this.getToken(systemUser.getUsername(), systemUser.getPassword());
httpHeaders.put("X-Access-Token", Lists.newArrayList(token));
httpHeaders.put("Content-Type",Lists.newArrayList("application/json"));
@ -382,7 +382,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
SwaggerInfo info = new SwaggerInfo();
info.setDescription("OpenAPI 接口列表");
info.setVersion("3.8.0");
info.setVersion("3.8.1");
info.setTitle("OpenAPI 接口列表");
info.setTermsOfService("https://jeecg.com");

View File

@ -1,35 +1,22 @@
package org.jeecg.modules.openapi.controller;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.modules.openapi.entity.OpenApiPermission;
import org.jeecg.modules.openapi.service.OpenApiPermissionService;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/openapi/permission")
public class OpenApiPermissionController extends JeecgController<OpenApiPermission, OpenApiPermissionService> {
@PostMapping("add")
public Result add(@RequestBody OpenApiPermission openApiPermission) {
List<String> list = Arrays.asList(openApiPermission.getApiId().split(","));
if (CollectionUtil.isNotEmpty(list)) {
list.forEach(l->{
OpenApiPermission saveApiPermission = new OpenApiPermission();
saveApiPermission.setApiId(l);
saveApiPermission.setApiAuthId(openApiPermission.getApiAuthId());
service.save(saveApiPermission);
});
}
service.add(openApiPermission);
return Result.ok("保存成功");
}
@GetMapping("/list")
public Result list( String apiAuthId) {
return Result.ok(service.list(Wrappers.<OpenApiPermission>lambdaQuery().eq(OpenApiPermission::getApiAuthId,apiAuthId)));
@GetMapping("/getOpenApi")
public Result<?> getOpenApi( String apiAuthId) {
return service.getOpenApi(apiAuthId);
}
}

View File

@ -1,6 +1,7 @@
package org.jeecg.modules.openapi.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;
@ -95,4 +96,9 @@ public class OpenApi implements Serializable {
* 更新时间
*/
private Date updateTime;
/**
* 历史已选接口
*/
@TableField(exist = false)
private String ifCheckBox = "0";
}

View File

@ -1,6 +1,8 @@
package org.jeecg.modules.openapi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.openapi.entity.OpenApi;
import org.jeecg.modules.openapi.entity.OpenApiPermission;
import java.util.List;
@ -10,4 +12,8 @@ import java.util.List;
*/
public interface OpenApiPermissionService extends IService<OpenApiPermission> {
List<OpenApiPermission> findByAuthId(String authId);
Result<?> getOpenApi(String apiAuthId);
void add(OpenApiPermission openApiPermission);
}

View File

@ -1,21 +1,67 @@
package org.jeecg.modules.openapi.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.openapi.entity.OpenApi;
import org.jeecg.modules.openapi.entity.OpenApiPermission;
import org.jeecg.modules.openapi.mapper.OpenApiPermissionMapper;
import org.jeecg.modules.openapi.service.OpenApiPermissionService;
import org.jeecg.modules.openapi.service.OpenApiService;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @date 2024/12/19 17:44
*/
@Service
public class OpenApiPermissionServiceImpl extends ServiceImpl<OpenApiPermissionMapper, OpenApiPermission> implements OpenApiPermissionService {
@Resource
private OpenApiService openApiService;
@Override
public List<OpenApiPermission> findByAuthId(String authId) {
return baseMapper.selectList(Wrappers.lambdaQuery(OpenApiPermission.class).eq(OpenApiPermission::getApiAuthId, authId));
}
@Override
public Result<?> getOpenApi(String apiAuthId) {
List<OpenApi> openApis = openApiService.list();
if (CollectionUtil.isEmpty(openApis)) {
return Result.error("接口不存在");
}
List<OpenApiPermission> openApiPermissions = baseMapper.selectList(Wrappers.<OpenApiPermission>lambdaQuery().eq(OpenApiPermission::getApiAuthId, apiAuthId));
if (CollectionUtil.isNotEmpty(openApiPermissions)) {
Map<String, OpenApi> openApiMap = openApis.stream().collect(Collectors.toMap(OpenApi::getId, o -> o));
for (OpenApiPermission openApiPermission : openApiPermissions) {
OpenApi openApi = openApiMap.get(openApiPermission.getApiId());
if (openApi!=null) {
openApi.setIfCheckBox("1");
}
}
}
return Result.ok(openApis);
}
@Override
public void add(OpenApiPermission openApiPermission) {
this.remove(Wrappers.<OpenApiPermission>lambdaQuery().eq(OpenApiPermission::getApiAuthId, openApiPermission.getApiAuthId()));
List<String> list = Arrays.asList(openApiPermission.getApiId().split(","));
if (CollectionUtil.isNotEmpty(list)) {
list.forEach(l->{
if (StrUtil.isNotEmpty(l)){
OpenApiPermission saveApiPermission = new OpenApiPermission();
saveApiPermission.setApiId(l);
saveApiPermission.setApiAuthId(openApiPermission.getApiAuthId());
this.save(saveApiPermission);
}
});
}
}
}

View File

@ -3,8 +3,8 @@ package org.jeecg.modules.quartz.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 io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;

View File

@ -229,7 +229,7 @@ public class CommonController {
File file = new File(filePath);
if(!file.exists()){
response.setStatus(404);
log.error("文件["+imgPath+"]不存在..");
log.warn("文件["+imgPath+"]不存在..");
return;
//throw new RuntimeException();
}

View File

@ -1,7 +1,7 @@
package org.jeecg.modules.system.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.api.vo.Result;
@ -36,7 +36,7 @@ public class DuplicateCheckController {
* @return
*/
@RequestMapping(value = "/check", method = RequestMethod.GET)
@Operation(summary ="重复校验接口")
@Operation(summary="重复校验接口")
public Result<String> doDuplicateCheck(DuplicateCheckVo duplicateCheckVo, HttpServletRequest request) {
log.debug("----duplicate check------"+ duplicateCheckVo.toString());

View File

@ -5,8 +5,8 @@ import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.exceptions.ClientException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresRoles;
@ -70,7 +70,7 @@ public class LoginController {
private final String BASE_CHECK_CODES = "qwertyuiplkjhgfdsazxcvbnmQWERTYUPLKJHGFDSAZXCVBNM1234567890";
@Operation(summary = "登录接口")
@Operation(summary="登录接口")
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Result<JSONObject> login(@RequestBody SysLoginModel sysLoginModel, HttpServletRequest request){
Result<JSONObject> result = new Result<JSONObject>();
@ -404,7 +404,7 @@ public class LoginController {
* @param jsonObject
* @return
*/
@Operation(summary = "手机号登录接口")
@Operation(summary="手机号登录接口")
@PostMapping("/phoneLogin")
public Result<JSONObject> phoneLogin(@RequestBody JSONObject jsonObject, HttpServletRequest request) {
Result<JSONObject> result = new Result<JSONObject>();
@ -523,7 +523,7 @@ public class LoginController {
* @param response
* @param key
*/
@Operation(summary = "获取验证码")
@Operation(summary="获取验证码")
@GetMapping(value = "/randomImage/{key}")
public Result<String> randomImage(HttpServletResponse response,@PathVariable("key") String key){
Result<String> res = new Result<String>();

View File

@ -534,6 +534,7 @@ public class SysAnnouncementController {
*/
@RequestMapping(value = "/vue3List", method = RequestMethod.GET)
public Result<List<SysAnnouncement>> vue3List(@RequestParam(name="fromUser", required = false) String fromUser,
@RequestParam(name="busType", required = false) String busType,
@RequestParam(name="starFlag", required = false) String starFlag,
@RequestParam(name="rangeDateKey", required = false) String rangeDateKey,
@RequestParam(name="beginDate", required = false) String beginDate,
@ -562,7 +563,7 @@ public class SysAnnouncementController {
}
// 2、根据条件查询用户的通知消息
List<SysAnnouncement> ls = this.sysAnnouncementService.querySysMessageList(pageSize, pageNo, fromUser, starFlag, beginTime, endTime);
List<SysAnnouncement> ls = this.sysAnnouncementService.querySysMessageList(pageSize, pageNo, fromUser, starFlag,busType, beginTime, endTime);
// 3、设置当前页的消息为已读
if (!CollectionUtils.isEmpty(ls)) {

View File

@ -4,8 +4,8 @@ import com.alibaba.fastjson.JSONObject;
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 io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;

View File

@ -3,8 +3,8 @@ package org.jeecg.modules.system.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 io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.dto.DataLogDTO;

View File

@ -7,8 +7,8 @@ import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
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 io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;

View File

@ -7,8 +7,6 @@ import jakarta.servlet.http.HttpServletResponse;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
@ -35,6 +33,8 @@ import org.jeecg.modules.system.service.ISysPermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
/**
* @Description: 部门权限表
@ -71,7 +71,7 @@ public class SysDepartPermissionController extends JeecgController<SysDepartPerm
* @param req
* @return
*/
@Operation(summary ="部门权限表-分页列表查询")
@Operation(summary="部门权限表-分页列表查询")
@GetMapping(value = "/list")
public Result<?> queryPageList(SysDepartPermission sysDepartPermission,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@ -89,7 +89,7 @@ public class SysDepartPermissionController extends JeecgController<SysDepartPerm
* @param sysDepartPermission
* @return
*/
@Operation(summary ="部门权限表-添加")
@Operation(summary="部门权限表-添加")
@PostMapping(value = "/add")
public Result<?> add(@RequestBody SysDepartPermission sysDepartPermission) {
sysDepartPermissionService.save(sysDepartPermission);
@ -102,7 +102,7 @@ public class SysDepartPermissionController extends JeecgController<SysDepartPerm
* @param sysDepartPermission
* @return
*/
@Operation(summary ="部门权限表-编辑")
@Operation(summary="部门权限表-编辑")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<?> edit(@RequestBody SysDepartPermission sysDepartPermission) {
sysDepartPermissionService.updateById(sysDepartPermission);
@ -115,7 +115,7 @@ public class SysDepartPermissionController extends JeecgController<SysDepartPerm
* @param id
* @return
*/
@Operation(summary ="部门权限表-通过id删除")
@Operation(summary="部门权限表-通过id删除")
@DeleteMapping(value = "/delete")
public Result<?> delete(@RequestParam(name="id",required=true) String id) {
sysDepartPermissionService.removeById(id);
@ -128,7 +128,7 @@ public class SysDepartPermissionController extends JeecgController<SysDepartPerm
* @param ids
* @return
*/
@Operation(summary ="部门权限表-批量删除")
@Operation(summary="部门权限表-批量删除")
@DeleteMapping(value = "/deleteBatch")
public Result<?> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
this.sysDepartPermissionService.removeByIds(Arrays.asList(ids.split(",")));
@ -141,7 +141,7 @@ public class SysDepartPermissionController extends JeecgController<SysDepartPerm
* @param id
* @return
*/
@Operation(summary ="部门权限表-通过id查询")
@Operation(summary="部门权限表-通过id查询")
@GetMapping(value = "/queryById")
public Result<?> queryById(@RequestParam(name="id",required=true) String id) {
SysDepartPermission sysDepartPermission = sysDepartPermissionService.getById(id);

View File

@ -7,8 +7,6 @@ import jakarta.servlet.http.HttpServletResponse;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
@ -30,6 +28,8 @@ import org.jeecg.common.system.base.controller.JeecgController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
/**
* @Description: 部门角色
@ -69,7 +69,7 @@ public class SysDepartRoleController extends JeecgController<SysDepartRole, ISys
* @param req
* @return
*/
@Operation(summary = "部门角色-分页列表查询")
@Operation(summary="部门角色-分页列表查询")
@GetMapping(value = "/list")
public Result<?> queryPageList(SysDepartRole sysDepartRole,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@ -110,7 +110,7 @@ public class SysDepartRoleController extends JeecgController<SysDepartRole, ISys
* @return
*/
@RequiresPermissions("system:depart:role:add")
@Operation(summary ="部门角色-添加")
@Operation(summary="部门角色-添加")
@PostMapping(value = "/add")
public Result<?> add(@RequestBody SysDepartRole sysDepartRole) {
sysDepartRoleService.save(sysDepartRole);
@ -123,7 +123,7 @@ public class SysDepartRoleController extends JeecgController<SysDepartRole, ISys
* @param sysDepartRole
* @return
*/
@Operation(summary ="部门角色-编辑")
@Operation(summary="部门角色-编辑")
@RequiresPermissions("system:depart:role:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<?> edit(@RequestBody SysDepartRole sysDepartRole) {
@ -138,7 +138,7 @@ public class SysDepartRoleController extends JeecgController<SysDepartRole, ISys
* @return
*/
@AutoLog(value = "部门角色-通过id删除")
@Operation(summary ="部门角色-通过id删除")
@Operation(summary="部门角色-通过id删除")
@RequiresPermissions("system:depart:role:delete")
@DeleteMapping(value = "/delete")
public Result<?> delete(@RequestParam(name="id",required=true) String id) {
@ -153,7 +153,7 @@ public class SysDepartRoleController extends JeecgController<SysDepartRole, ISys
* @return
*/
@AutoLog(value = "部门角色-批量删除")
@Operation(summary ="部门角色-批量删除")
@Operation(summary="部门角色-批量删除")
@RequiresPermissions("system:depart:role:deleteBatch")
@DeleteMapping(value = "/deleteBatch")
public Result<?> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
@ -168,7 +168,7 @@ public class SysDepartRoleController extends JeecgController<SysDepartRole, ISys
* @param id
* @return
*/
@Operation(summary ="部门角色-通过id查询")
@Operation(summary="部门角色-通过id查询")
@GetMapping(value = "/queryById")
public Result<?> queryById(@RequestParam(name="id",required=true) String id) {
SysDepartRole sysDepartRole = sysDepartRoleService.getById(id);

View File

@ -7,8 +7,8 @@ import java.util.Date;
import jakarta.servlet.http.HttpServletRequest;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
@ -162,7 +162,7 @@ public class SysDictItemController {
* @return
*/
@RequestMapping(value = "/dictItemCheck", method = RequestMethod.GET)
@Operation(summary ="字典重复校验接口")
@Operation(summary="字典重复校验接口")
public Result<Object> doDictItemCheck(SysDictItem sysDictItem, HttpServletRequest request) {
Long num = Long.valueOf(0);
LambdaQueryWrapper<SysDictItem> queryWrapper = new LambdaQueryWrapper<SysDictItem>();

View File

@ -5,8 +5,8 @@ import com.alibaba.fastjson.JSONObject;
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 io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;

View File

@ -3,8 +3,8 @@ package org.jeecg.modules.system.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 io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;

View File

@ -5,8 +5,8 @@ import com.alibaba.fastjson.JSON;
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 io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;

View File

@ -4,8 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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 io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

View File

@ -3,8 +3,8 @@ package org.jeecg.modules.system.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 io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;

View File

@ -986,4 +986,16 @@ public class SysTenantController {
}
return result;
}
/**
* 目前只给敲敲云人员与部门下的用户删除使用
*
* 删除用户
*/
@DeleteMapping("/deleteUser")
public Result<String> deleteUser(@RequestBody SysUser sysUser,HttpServletRequest request){
Integer tenantId = oConvertUtils.getInteger(TokenUtils.getTenantIdByRequest(request), null);
sysTenantService.deleteUser(sysUser, tenantId);
return Result.ok("删除用户成功");
}
}

View File

@ -1727,7 +1727,7 @@ public class SysUserController {
}
return result;
}
/**
* 根据关键词搜索部门和用户【low-app】
* @param keyword
@ -1847,7 +1847,7 @@ public class SysUserController {
public Result<?> importAppUser(HttpServletRequest request, HttpServletResponse response)throws IOException {
return sysUserService.importAppUser(request);
}
/**
* 更改手机号(敲敲云个人设置专用)
*

View File

@ -45,7 +45,6 @@ public class SysUserOnlineController {
public ISysUserService userService;
@Autowired
private SysBaseApiImpl sysBaseApi;
@Resource
private BaseCommonService baseCommonService;

View File

@ -283,7 +283,7 @@ public class ThirdLoginController {
* @param jsonObject
* @return
*/
@Operation(summary ="手机号登录接口")
@Operation(summary="手机号登录接口")
@PostMapping("/bindingThirdPhone")
@ResponseBody
public Result<String> bindingThirdPhone(@RequestBody JSONObject jsonObject) {

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -23,7 +24,7 @@ import java.util.Date;
@TableName("sys_check_rule")
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Schema(description = "编码校验规则")
@Schema(description="编码校验规则")
public class SysCheckRule {
/**

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -22,7 +23,7 @@ import org.springframework.format.annotation.DateTimeFormat;
@TableName("sys_data_source")
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Schema(description = "多数据源管理")
@Schema(description="多数据源管理")
public class SysDataSource {
/**

View File

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;

View File

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;

View File

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -24,7 +25,7 @@ import org.jeecgframework.poi.excel.annotation.Excel;
@TableName("sys_depart_role_permission")
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Schema( description="部门角色权限")
@Schema(description="部门角色权限")
public class SysDepartRolePermission {
/**id*/

View File

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -21,7 +22,7 @@ import org.springframework.format.annotation.DateTimeFormat;
@TableName("sys_fill_rule")
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Schema(description = "填值规则")
@Schema(description="填值规则")
public class SysFillRule {
/**

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;

View File

@ -5,11 +5,12 @@ import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import org.jeecgframework.poi.excel.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

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