v3.9.1 后台代码

This commit is contained in:
JEECG
2026-01-21 19:02:45 +08:00
parent 3735ca1687
commit e2402c75b0
113 changed files with 9495 additions and 109830 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -177,6 +177,8 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//仪表盘(按钮通信)
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
//App vue3版本查询版本接口
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//性能监控——安全隐患泄露TOEKNdurid连接池也有
//filterChainDefinitionMap.put("/actuator/**", "anon");
@ -188,6 +190,8 @@ public class ShiroConfig {
// 企业微信证书排除
filterChainDefinitionMap.put("/WW_verify*", "anon");
filterChainDefinitionMap.put("/openapi/call/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
@ -227,6 +231,11 @@ public class ShiroConfig {
registration.addUrlPatterns("/airag/app/debug");
registration.addUrlPatterns("/airag/app/prompt/generate");
registration.addUrlPatterns("/airag/chat/receive/**");
// 添加SSE接口的异步支持
registration.addUrlPatterns("/airag/extData/evaluator/debug");
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateChartSse");
registration.addUrlPatterns("/drag/onlDragDatasetHead/updateChartOptSse");
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateSqlSse");
//支持异步
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);

View File

@ -1,6 +1,5 @@
package org.jeecg.config.sign.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
@ -13,7 +12,12 @@ import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestBody;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 基于AOP的签名验证切面
@ -56,7 +60,22 @@ public class SignatureCheckAspect {
log.info("签名验证已禁用,跳过");
return;
}
// update-begin---author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数解决签名校验时读取请求体为空的问题
Object bodyParam = null;
Object[] args = point.getArgs();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
Annotation[] annotations = parameterAnnotations[i];
boolean hasRequestBodyAnnotation = Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType().equals(RequestBody.class));
if (hasRequestBodyAnnotation) {
// 捕获携带@RequestBody注解的参数供签名校验使用
bodyParam = arg;
}
}
// update-end-----author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数解决签名校验时读取请求体为空的问题
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
log.error("无法获取请求上下文");
@ -68,7 +87,7 @@ public class SignatureCheckAspect {
try {
// 直接调用SignAuthInterceptor的验证逻辑
signAuthInterceptor.validateSignature(request);
signAuthInterceptor.validateSignature(request, bodyParam);
log.info("AOP签名验证通过");
} catch (IllegalArgumentException e) {

View File

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

View File

@ -1,10 +1,12 @@
package org.jeecg.config.sign.interceptor;
import com.alibaba.fastjson.JSON;
import java.io.PrintWriter;
import java.util.SortedMap;
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.DateUtils;
@ -14,8 +16,9 @@ import org.jeecg.config.sign.util.HttpUtils;
import org.jeecg.config.sign.util.SignUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.PrintWriter;
import java.util.SortedMap;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
/**
* 签名拦截器
@ -47,7 +50,7 @@ public class SignAuthInterceptor implements HandlerInterceptor {
return false;
}
}
/**
* 签名验证核心逻辑
* 提取出来供AOP切面复用
@ -55,12 +58,22 @@ public class SignAuthInterceptor implements HandlerInterceptor {
* @throws IllegalArgumentException 验证失败时抛出异常
*/
public void validateSignature(HttpServletRequest request) throws IllegalArgumentException {
validateSignature(request, null);
}
/**
* 签名验证核心逻辑
* 提取出来供AOP切面复用
* @param request HTTP请求
* @throws IllegalArgumentException 验证失败时抛出异常
*/
public void validateSignature(HttpServletRequest request, Object bodyParam) throws IllegalArgumentException {
try {
log.debug("开始签名验证: {} {}", request.getMethod(), request.getRequestURI());
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
log.debug("提取参数: {}", allParams);
//对参数进行签名验证

View File

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

View File

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