v3.9.0版本合并commit

This commit is contained in:
JEECG
2025-12-02 22:25:15 +08:00
741 changed files with 33884 additions and 181862 deletions

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-module</artifactId>
<version>3.8.3</version>
<version>3.9.0</version>
</parent>
<artifactId>jeecg-boot-module-airag</artifactId>
@ -31,6 +31,8 @@
</repositories>
<properties>
<kotlin.version>2.2.0</kotlin.version>
<liteflow.version>2.15.0</liteflow.version>
<apache-tika.version>2.9.1</apache-tika.version>
</properties>
@ -73,10 +75,24 @@
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-aiflow</artifactId>
<version>3.8.3.1</version>
<version>3.9.0.1</version>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
<exclusion>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
<exclusion>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- beigin 这两个依赖太多每个50M左右,如果你发布需要使用,请<scope>provided</scope>删掉 -->
<!-- begin 注意:这几个依赖体积较大,每个50MB。若发布需要使用,请<scope>provided</scope> 删除 -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
@ -89,7 +105,14 @@
<version>${liteflow.version}</version>
<scope>provided</scope>
</dependency>
<!-- end 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId>
<version>${liteflow.version}</version>
<scope>provided</scope>
</dependency>
<!-- end 注意这几个依赖体积较大每个约50MB。若发布时需要使用请将 <scope>provided</scope> 删除 -->
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
@ -122,7 +145,7 @@
</exclusions>
</dependency>
<!-- aiflow 脚本依赖 -->
<!-- langChain4j model support -->
<dependency>
<groupId>dev.langchain4j</groupId>
@ -164,6 +187,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
</dependency>
<!-- langChain4j vextor support -->
<dependency>
<groupId>org.jeecgframework</groupId>

View File

@ -38,4 +38,10 @@ public class AiAppConsts {
*/
public static final String APP_TYPE_CHAT_FLOW = "chatFLow";
/**
* 应用元数据:流程输入参数
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
}

View File

@ -167,6 +167,12 @@ public class AiragApp implements Serializable {
@Schema(description = "元数据")
private java.lang.String metadata;
/**
* 插件 [{pluginId: '123213', pluginName: 'xxxx', category: 'mcp'}]
*/
@Schema(description = "插件")
private java.lang.String plugins;
/**
* 知识库ids
*/

View File

@ -1,10 +1,13 @@
package org.jeecg.modules.airag.app.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.data.image.Image;
import dev.langchain4j.data.message.*;
import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.service.TokenStream;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
@ -23,16 +26,19 @@ import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
import org.jeecg.modules.airag.common.utils.AiragLocalCache;
import org.jeecg.modules.airag.common.vo.LlmPlugin;
import org.jeecg.modules.airag.common.vo.MessageHistory;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
@ -40,7 +46,6 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
@ -78,6 +83,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
@Autowired
JeecgToolsProvider jeecgToolsProvider;
@Autowired
AiragModelMapper airagModelMapper;
/**
* 重新接收消息
*/
@ -102,6 +110,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
}
//update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 保存工作流入参配置(如果有)
if (oConvertUtils.isObjectNotEmpty(chatSendParams.getFlowInputs())) {
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
}
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 发送消息
return doChat(chatConversation, topicId, chatSendParams);
}
@ -117,6 +131,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
AiragApp app = appDebugParams.getApp();
app.setId("__DEBUG_APP");
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
//update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 保存工作流入参配置(如果有)
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
chatConversation.setFlowInputs(appDebugParams.getFlowInputs());
}
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 发送消息
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
//保存会话
@ -237,7 +257,33 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isObjectEmpty(chatConversation)) {
return Result.ok(Collections.emptyList());
}
return Result.ok(chatConversation.getMessages());
//update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 返回消息列表和会话设置信息
Map<String, Object> result = new HashMap<>();
// 过滤掉工具调用相关的消息(前端不需要展示)
List<MessageHistory> messages = chatConversation.getMessages();
if (oConvertUtils.isObjectNotEmpty(messages)) {
messages = messages.stream()
.filter(msg -> !AiragConsts.MESSAGE_ROLE_TOOL.equals(msg.getRole()))
.map(msg -> {
// 克隆消息对象,移除工具执行请求信息(前端不需要)
MessageHistory displayMsg = MessageHistory.builder()
.conversationId(msg.getConversationId())
.topicId(msg.getTopicId())
.role(msg.getRole())
.content(msg.getContent())
.images(msg.getImages())
.datetime(msg.getDatetime())
.build();
// 不设置toolExecutionRequests和toolExecutionResult
return displayMsg;
})
.collect(Collectors.toList());
}
result.put("messages", messages);
result.put("flowInputs", chatConversation.getFlowInputs());
return Result.ok(result);
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
}
@Override
@ -258,6 +304,51 @@ public class AiragChatServiceImpl implements IAiragChatService {
@Override
public Result<?> initChat(String appId) {
AiragApp app = airagAppMapper.getByIdIgnoreTenant(appId);
//update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
if(AiAppConsts.APP_TYPE_CHAT_FLOW.equalsIgnoreCase(app.getType())) {
AiragFlow flow = airagFlowService.getById(app.getFlowId());
String flowMetadata = flow.getMetadata();
if(oConvertUtils.isNotEmpty(flowMetadata)) {
JSONObject flowMetadataJson = JSONObject.parseObject(flowMetadata);
JSONArray flowMetadataInputs = flowMetadataJson.getJSONArray(FlowConsts.FLOW_METADATA_INPUTS);
if(oConvertUtils.isObjectNotEmpty(flowMetadataInputs)) {
String appMetadataStr = app.getMetadata();
JSONObject appMetadataJson;
if(oConvertUtils.isEmpty(appMetadataStr)){
appMetadataJson = new JSONObject();
} else {
appMetadataJson = JSONObject.parseObject(appMetadataStr);
}
appMetadataJson.put(AiAppConsts.APP_METADATA_FLOW_INPUTS, flowMetadataInputs);
app.setMetadata(appMetadataJson.toJSONString());
}
}
}
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
//update-begin---author:chenrui ---date:202501XX for在initChat接口中返回模型供应商信息避免前端多次调用模型查询接口------------
// 如果应用有模型ID查询模型信息并将供应商、类型、名称等信息添加到metadata中
if (oConvertUtils.isNotEmpty(app.getModelId())) {
AiragModel model = airagModelMapper.getByIdIgnoreTenant(app.getModelId());
if (model != null) {
String appMetadataStr = app.getMetadata();
JSONObject appMetadataJson;
if(oConvertUtils.isEmpty(appMetadataStr)){
appMetadataJson = new JSONObject();
} else {
appMetadataJson = JSONObject.parseObject(appMetadataStr);
}
// 将模型信息添加到metadata中
JSONObject modelInfo = new JSONObject();
modelInfo.put("provider", model.getProvider());
modelInfo.put("modelType", model.getModelType());
modelInfo.put("modelName", model.getModelName());
appMetadataJson.put("modelInfo", modelInfo);
app.setMetadata(appMetadataJson.toJSONString());
}
}
//update-end---author:chenrui ---date:202501XX for在initChat接口中返回模型供应商信息避免前端多次调用模型查询接口------------
return Result.ok(app);
}
@ -541,7 +632,30 @@ public class AiragChatServiceImpl implements IAiragChatService {
chatMessage = UserMessage.from(contents);
break;
case AiragConsts.MESSAGE_ROLE_AI:
chatMessage = new AiMessage(history.getContent());
// 重建AI消息包括工具执行请求
if (oConvertUtils.isObjectNotEmpty(history.getToolExecutionRequests())) {
// 有工具执行请求重建带工具调用的AiMessage
List<ToolExecutionRequest> toolRequests = history.getToolExecutionRequests().stream()
.map(toolReq -> ToolExecutionRequest.builder()
.id(toolReq.getId())
.name(toolReq.getName())
.arguments(toolReq.getArguments())
.build())
.collect(Collectors.toList());
chatMessage = AiMessage.from(history.getContent(), toolRequests);
} else {
chatMessage = new AiMessage(history.getContent());
}
break;
case AiragConsts.MESSAGE_ROLE_TOOL:
// 重建工具执行结果消息
// 需要重建ToolExecutionRequest第一个参数是request对象第二个参数是result字符串
ToolExecutionRequest recreatedRequest = ToolExecutionRequest.builder()
.id(history.getContent()) // content字段存储的是工具执行的id
.name("unknown") // 工具名称在重建时不重要因为主要用于AI理解结果
.arguments("{}")
.build();
chatMessage = ToolExecutionResultMessage.from(recreatedRequest, history.getToolExecutionResult());
break;
}
if (null == chatMessage) {
@ -599,7 +713,26 @@ public class AiragChatServiceImpl implements IAiragChatService {
historyMessage.setImages(images);
} else if (message.type().equals(ChatMessageType.AI)) {
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
historyMessage.setContent(((AiMessage) message).text());
AiMessage aiMessage = (AiMessage) message;
historyMessage.setContent(aiMessage.text());
// 处理工具执行请求
if (oConvertUtils.isObjectNotEmpty(aiMessage.toolExecutionRequests())) {
List<MessageHistory.ToolExecutionRequestHistory> toolRequests = new ArrayList<>();
for (ToolExecutionRequest request : aiMessage.toolExecutionRequests()) {
toolRequests.add(MessageHistory.ToolExecutionRequestHistory.from(
request.id(),
request.name(),
request.arguments()
));
}
historyMessage.setToolExecutionRequests(toolRequests);
}
} else if (message.type().equals(ChatMessageType.TOOL_EXECUTION_RESULT)) {
// 工具执行结果消息
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_TOOL);
ToolExecutionResultMessage toolMessage = (ToolExecutionResultMessage) message;
historyMessage.setContent(toolMessage.id());
historyMessage.setToolExecutionResult(toolMessage.text());
}
histories.add(historyMessage);
chatConversation.setMessages(histories);
@ -648,11 +781,15 @@ public class AiragChatServiceImpl implements IAiragChatService {
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
} else {
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
sendWithAppChat(requestId, messages, chatConversation, topicId);
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
}
} else {
// 发消息
sendWithDefault(requestId, chatConversation, topicId, null, messages, null);
AIChatParams aiChatParams = new AIChatParams();
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
}
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams);
}
// 发送就绪消息
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
@ -698,6 +835,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_HISTORY, histories);
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_QUESTION, sendParams.getContent());
flowInputParams.put(FlowConsts.FLOW_INPUT_PARAM_IMAGES, sendParams.getImages());
//update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 添加工作流的额外参数从conversation的flowInputs中读取
if (oConvertUtils.isObjectNotEmpty(chatConversation.getFlowInputs())) {
flowInputParams.putAll(chatConversation.getFlowInputs());
}
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
flowRunParams.setInputParams(flowInputParams);
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
flowRunParams.setHttpRequest(httpRequest);
@ -762,11 +907,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @param messages
* @param chatConversation
* @param topicId
* @param sendParams
* @return
* @author chenrui
* @date 2025/2/28 10:41
*/
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
AiragApp aiApp = chatConversation.getApp();
String modelId = aiApp.getModelId();
AssertUtils.assertNotEmpty("请先选择模型", modelId);
@ -799,6 +945,31 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
}
}
// AI应用插件支持MCP和自定义插件
String plugins = aiApp.getPlugins();
if (oConvertUtils.isNotEmpty(plugins)) {
List<String> pluginIds = new ArrayList<>();
JSONArray pluginArray = JSONArray.parseArray(plugins);
pluginArray.stream().filter(Objects::nonNull)
.map(o -> JSONObject.parseObject(o.toString(), LlmPlugin.class))
.forEach(plugin -> {
// 支持MCP和插件类型
if (plugin.getCategory().equals(AiragConsts.PLUGIN_CATEGORY_MCP)
|| plugin.getCategory().equals(AiragConsts.PLUGIN_CATEGORY_PLUGIN)) {
pluginIds.add(plugin.getPluginId());
}
});
if (oConvertUtils.isNotEmpty(pluginIds)) {
aiChatParams.setPluginIds(pluginIds);
}
}
// 设置网络搜索参数(如果前端传递了)
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
}
// 打印流程耗时日志
printChatDuration(requestId, "构造应用自定义参数完成");
// 发消息
@ -828,6 +999,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
aiChatParams.setReturnThinking(true);
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
TokenStream chatStream;
try {
@ -861,20 +1034,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
AtomicBoolean isThinking = new AtomicBoolean(false);
// ai聊天响应逻辑
chatStream.onPartialResponse((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
//update-begin---author:wangshuai---date:2025-11-07---for:[issues/8506]/[issues/8260]/[issues/8166]新增推理模型的支持---
if(isThinking.get()){
//思考过程结束
this.sendThinkEnd(requestId, chatConversation, topicId);
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
}
}
//update-end---author:wangshuai---date:2025-11-07---for:[issues/8506]/[issues/8260]/[issues/8166]新增推理模型的支持---
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message(resMessage).build();
eventData.setData(messageEventData);
@ -886,6 +1052,48 @@ public class AiragChatServiceImpl implements IAiragChatService {
return;
}
sendMessage2Client(emitter, eventData);
}).onToolExecuted((toolExecution) -> {
// 打印工具执行结果
log.debug("[AI应用]工具执行结果: toolName={}, toolId={}, result={}",
toolExecution.request().name(),
toolExecution.request().id(),
toolExecution.result());
// 将工具执行结果存储到消息历史中
ToolExecutionResultMessage toolResultMessage = ToolExecutionResultMessage.from(
toolExecution.request(),
toolExecution.result()
);
appendMessage(messages, toolResultMessage, chatConversation, topicId);
}).onIntermediateResponse((chatResponse) -> {
// 中间响应包含tool_calls的AI消息
AiMessage aiMessage = chatResponse.aiMessage();
if (aiMessage != null && oConvertUtils.isObjectNotEmpty(aiMessage.toolExecutionRequests())) {
// 保存包含工具调用请求的AI消息
log.debug("[AI应用]保存包含工具调用的AI消息: toolCallsCount={}", aiMessage.toolExecutionRequests().size());
appendMessage(messages, aiMessage, chatConversation, topicId);
}
}).onPartialThinking((partialThinking) -> {
try {
if (oConvertUtils.isEmpty(partialThinking)) {
return;
}
isThinking.set(true);
String text = partialThinking.text();
// 构造事件数据EVENT_THINKING 以便前端统一处理)
EventData thinkingEvent = new EventData(requestId, null, EventData.EVENT_THINKING, chatConversation.getId(), topicId);
thinkingEvent.setData(EventMessageData.builder().message(text).build());
thinkingEvent.setRequestId(requestId);
// 获取当前缓存的 emitter
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]思考过程发送失败SSE 已关闭: {}", requestId);
return;
}
// 发送给客户端并缓存历史
sendMessage2Client(emitter, thinkingEvent);
} catch (Exception e) {
log.error("发送思考过程异常", e);
}
}).onCompleteResponse((responseMessage) -> {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息完成");
@ -907,9 +1115,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 保存会话
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());
@ -966,6 +1171,26 @@ public class AiragChatServiceImpl implements IAiragChatService {
}).start();
}
/**
* 发送思考过程结束
*
* @param requestId
* @param chatConversation
* @param topicId
*/
private void sendThinkEnd(String requestId, ChatConversation chatConversation, String topicId) {
EventData eventData = new EventData(requestId, null, EventData.EVENT_THINKING_END, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message("").build();
eventData.setData(messageEventData);
eventData.setRequestId(requestId);
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
sendMessage2Client(emitter, eventData);
}
/**
* 发送消息到客户端
*

View File

@ -6,6 +6,7 @@ import org.jeecg.modules.airag.common.vo.MessageHistory;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @Description: 聊天会话
@ -39,4 +40,11 @@ public class ChatConversation {
* 创建时间
*/
private Date createTime;
/**
* 流程入参配置(工作流的额外参数设置)
* key: 参数field, value: 参数值
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/
private Map<String, Object> flowInputs;
}

View File

@ -4,6 +4,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* @Description: 发送消息的入参
@ -46,4 +47,16 @@ public class ChatSendParams {
*/
private List<String> images;
/**
* 工作流额外入参配置
* key: 参数field, value: 参数值
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/
private Map<String, Object> flowInputs;
/**
* 是否开启网络搜索(仅千问模型支持)
*/
private Boolean enableSearch;
}

View File

@ -80,4 +80,9 @@ public class LLMConsts {
*/
public static final String KNOWLEDGE_DOC_METADATA_SOURCES_PATH = "sourcesPath";
/**
* DEEPSEEK推理模型
*/
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
}

View File

@ -0,0 +1,191 @@
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 io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.service.IAiragMcpService;
import org.jeecg.modules.airag.llm.dto.SaveToolsDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
/**
* @Description: MCP
* @Author: jeecg-boot
* @Date: 2025-10-20
* @Version: V1.0
*/
@Tag(name = "MCP")
@RestController("airagMcpController")
@RequestMapping("/airag/airagMcp")
@Slf4j
public class AiragMcpController extends JeecgController<AiragMcp, IAiragMcpService> {
@Autowired
private IAiragMcpService airagMcpService;
/**
* 分页列表查询
*
* @param airagMcp
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@Operation(summary = "MCP-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<AiragMcp>> queryPageList(AiragMcp airagMcp,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<AiragMcp> queryWrapper = QueryGenerator.initQueryWrapper(airagMcp, req.getParameterMap());
Page<AiragMcp> page = new Page<AiragMcp>(pageNo, pageSize);
IPage<AiragMcp> pageList = airagMcpService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 保存
*
* @param airagMcp
* @return
*/
@Operation(summary = "MCP-保存")
@PostMapping(value = "/save")
public Result<String> save(@RequestBody AiragMcp airagMcp) {
return airagMcpService.edit(airagMcp);
}
/**
* 保存并同步
*
* @param airagMcp
* @return
* @author chenrui
* @date 2025/10/21 10:54
*/
@Operation(summary = "MCP-保存并同步")
@PostMapping(value = "/saveAndSync")
public Result<?> saveAndSync(@RequestBody AiragMcp airagMcp) {
Result<String> saveResult = airagMcpService.edit(airagMcp);
if (!saveResult.isSuccess()) {
return saveResult;
}
String id = airagMcp.getId();
if (id == null || id.trim().isEmpty()) {
return Result.error("保存失败");
}
return airagMcpService.sync(id);
}
/**
* 同步MCP信息
*
* @param id
* @return
* @author chenrui
* @date 2025/10/20 20:09
*/
@Operation(summary = "MCP-同步MCP信息")
@PostMapping(value = "/sync/{id}")
public Result<?> sync(@PathVariable(name = "id", required = true) String id) {
return airagMcpService.sync(id);
}
/**
* 启用/禁用MCP信息
*
* @param action 启用enable禁用disable
* @return
* @author chenrui
* @date 2025/10/20 20:13
*/
@Operation(summary = "MCP-启用/禁用MCP信息")
@PostMapping(value = "/status/{id}/{action}")
public Result<?> toggleStatus(@PathVariable(name = "id",required = true) String id,
@PathVariable(name = "action", required = true) String action) {
return airagMcpService.toggleStatus(id,action);
}
/**
* 保存插件工具
* for [QQYUN-12453]【AI】支持插件
* @param dto 包含插件ID和工具列表JSON字符串的DTO
* @return
* @author chenrui
* @date 2025/10/30
*/
@Operation(summary = "MCP-保存插件工具")
@PostMapping(value = "/saveTools")
public Result<String> saveTools(@RequestBody SaveToolsDTO dto) {
return airagMcpService.saveTools(dto.getId(), dto.getTools());
}
/**
* 通过id删除
*
* @param id
* @return
*/
@Operation(summary = "MCP-通过id删除")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
airagMcpService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
@Operation(summary = "MCP-通过id查询")
@GetMapping(value = "/queryById")
public Result<AiragMcp> queryById(@RequestParam(name = "id", required = true) String id) {
AiragMcp airagMcp = airagMcpService.getById(id);
if (airagMcp == null) {
return Result.error("未找到对应数据");
}
return Result.OK(airagMcp);
}
/**
* 导出excel
*
* @param request
* @param airagMcp
*/
// @PreAuthorize("@jps.requiresPermissions('llm:airag_mcp:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, AiragMcp airagMcp) {
return super.exportXls(request, airagMcp, AiragMcp.class, "MCP");
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
// @PreAuthorize("@jps.requiresPermissions('llm:airag_mcp:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, AiragMcp.class);
}
}

View File

@ -81,8 +81,9 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
// 默认未激活
if(oConvertUtils.isObjectEmpty(airagModel.getActivateFlag())){
airagModel.setActivateFlag(0);
} else {
airagModel.setActivateFlag(1);
}
airagModel.setActivateFlag(0);
airagModelService.save(airagModel);
return Result.OK("添加成功!");
}
@ -178,7 +179,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
}
}catch (Exception e){
log.error("测试模型连接失败", e);
return Result.error("测试模型连接失败" + e.getMessage());
return Result.error("测试模型连接失败,请检查模型配置是否正确!");
}
// 测试成功激活数据
airagModel.setActivateFlag(1);

View File

@ -0,0 +1,23 @@
package org.jeecg.modules.airag.llm.dto;
import lombok.Data;
/**
* 保存插件工具DTO
* fro [QQYUN-12453]【AI】支持插件
* @author chenrui
* @date 2025/10/30
*/
@Data
public class SaveToolsDTO {
/**
* 插件ID
*/
private String id;
/**
* 工具列表JSON字符串
*/
private String tools;
}

View File

@ -0,0 +1,138 @@
package org.jeecg.modules.airag.llm.entity;
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;
import lombok.experimental.Accessors;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
/**
* @Description: MCP
* @Author: jeecg-boot
* @Date: 2025-10-20
* @Version: V1.0
*/
@Data
@TableName("airag_mcp")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description = "MCP")
public class AiragMcp implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "id")
private java.lang.String id;
/**
* 应用图标
*/
@Excel(name = "应用图标", width = 15)
@Schema(description = "应用图标")
private java.lang.String icon;
/**
* 名称
*/
@Excel(name = "名称", width = 15)
@Schema(description = "名称")
private java.lang.String name;
/**
* 描述
*/
@Excel(name = "描述", width = 15)
@Schema(description = "描述")
private java.lang.String descr;
/**
* 类型plugin=插件mcp=MCP
* for [QQYUN-12453]【AI】支持插件
*/
@Excel(name = "类型plugin=插件mcp=MCP", width = 15)
@Schema(description = "类型plugin=插件mcp=MCP")
private java.lang.String category;
/**
* mcp类型ssesse类型stdio标准类型
*/
@Excel(name = "mcp类型ssesse类型stdio标准类型", width = 15)
@Schema(description = "mcp类型ssesse类型stdio标准类型")
private java.lang.String type;
/**
* 服务端点SSE类型为URLstdio类型为命令
*/
@Excel(name = "服务端点SSE类型为URLstdio类型为命令", width = 15)
@Schema(description = "服务端点SSE类型为URLstdio类型为命令")
private java.lang.String endpoint;
/**
* 请求头sse类型、环境变量stdio类型
*/
@Excel(name = "请求头sse类型、环境变量stdio类型", width = 15)
@Schema(description = "请求头sse类型、环境变量stdio类型")
private java.lang.String headers;
/**
* 工具列表
*/
@Excel(name = "工具列表", width = 15)
@Schema(description = "工具列表")
private java.lang.String tools;
/**
* 状态enable=启用、disable=禁用)
*/
@Excel(name = "状态enable=启用、disable=禁用)", width = 15)
@Schema(description = "状态enable=启用、disable=禁用)")
private java.lang.String status;
/**
* 是否同步
*/
@Excel(name = "是否同步", width = 15)
@Schema(description = "是否同步")
private java.lang.Integer synced;
/**
* 元数据
*/
@Excel(name = "元数据", width = 15)
@Schema(description = "元数据")
private java.lang.String metadata;
/**
* 创建人
*/
@Schema(description = "创建人")
private java.lang.String createBy;
/**
* 创建日期
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建日期")
private java.util.Date createTime;
/**
* 更新人
*/
@Schema(description = "更新人")
private java.lang.String updateBy;
/**
* 更新日期
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新日期")
private java.util.Date updateTime;
/**
* 所属部门
*/
@Schema(description = "所属部门")
private java.lang.String sysOrgCode;
/**
* 租户id
*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private java.lang.String tenantId;
}

View File

@ -1,9 +1,12 @@
package org.jeecg.modules.airag.llm.handler;
import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.data.message.*;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.rag.query.router.QueryRouter;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.tool.ToolExecutor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.ai.handler.LLMHandler;
import org.jeecg.common.exception.JeecgBootException;
@ -12,7 +15,9 @@ import org.jeecg.common.util.oConvertUtils;
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.AiragMcp;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -25,6 +30,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
/**
* 大模型聊天工具类
@ -39,6 +45,9 @@ public class AIChatHandler implements IAIChatHandler {
@Autowired
AiragModelMapper airagModelMapper;
@Autowired
AiragMcpMapper airagMcpMapper;
@Autowired
EmbeddingHandler embeddingHandler;
@ -105,12 +114,22 @@ public class AIChatHandler implements IAIChatHandler {
// langchain4j 异常友好提示
String errMsg = "调用大模型接口失败,详情请查看后台日志。";
if (oConvertUtils.isNotEmpty(e.getMessage())) {
String exceptionMsg = e.getMessage();
// 检查是否是工具调用消息序列不完整的异常
if (exceptionMsg.contains("messages with role 'tool' must be a response to a preceeding message with 'tool_calls'")) {
errMsg = "消息序列不完整,可能是因为历史消息数量设置过小导致工具调用上下文丢失。建议增加历史消息数量后重试。";
log.error("AI模型调用异常: 工具调用消息序列不完整,建议增加历史消息数量。异常详情: {}", exceptionMsg, e);
throw new JeecgBootException(errMsg);
}
// 根据常见异常关键字做细致翻译
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (errMsg.contains(key)) {
errMsg = value;
break;
}
}
}
@ -247,6 +266,9 @@ public class AIChatHandler implements IAIChatHandler {
if (oConvertUtils.isObjectEmpty(params.getTimeout())) {
params.setTimeout(modelParams.getInteger("timeout"));
}
if (oConvertUtils.isObjectEmpty(params.getEnableSearch())) {
params.setEnableSearch(modelParams.getBoolean("enableSearch"));
}
}
// RAG
@ -266,9 +288,77 @@ public class AIChatHandler implements IAIChatHandler {
params.setTimeout(60);
}
//deepseek-reasoner 推理模型不支持插件tool
String modelName = airagModel.getModelName();
if(!LLMConsts.DEEPSEEK_REASONER.equals(modelName)){
// 插件/MCP处理
buildPlugins(params);
}
return params;
}
/**
* 构造插件和MCP工具
* for [QQYUN-12453]【AI】支持插件
* @param params
* @author chenrui
* @date 2025/10/31 14:04
*/
private void buildPlugins(AIChatParams params) {
List<String> pluginIds = params.getPluginIds();
if(oConvertUtils.isObjectNotEmpty(pluginIds)){
List<McpToolProvider> mcpToolProviders = new ArrayList<>();
Map<ToolSpecification, ToolExecutor> pluginTools = new HashMap<>();
for (String pluginId : pluginIds.stream().distinct().collect(Collectors.toList())) {
AiragMcp airagMcp = airagMcpMapper.selectById(pluginId);
if (airagMcp == null) {
continue;
}
String category = airagMcp.getCategory();
if (oConvertUtils.isEmpty(category)) {
// 兼容旧数据如果没有category字段默认为mcp
category = "mcp";
}
if ("mcp".equalsIgnoreCase(category)) {
// MCP类型构建McpToolProvider
McpToolProvider mcpToolProvider = buildMcpToolProvider(
airagMcp.getName(),
airagMcp.getType(),
airagMcp.getEndpoint(),
airagMcp.getHeaders()
);
if (mcpToolProvider != null) {
mcpToolProviders.add(mcpToolProvider);
}
} else if ("plugin".equalsIgnoreCase(category)) {
// 插件类型构建ToolSpecification和ToolExecutor
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(airagMcp, params.getCurrentHttpRequest());
if (tools != null && !tools.isEmpty()) {
pluginTools.putAll(tools);
}
}
}
// 设置MCP工具提供者
if (!mcpToolProviders.isEmpty()) {
params.setMcpToolProviders(mcpToolProviders);
}
// 设置插件工具
if (!pluginTools.isEmpty()) {
if (params.getTools() == null) {
params.setTools(new HashMap<>());
}
params.getTools().putAll(pluginTools);
}
}
}
@Override
public UserMessage buildUserMessage(String content, List<String> images) {
AssertUtils.assertNotEmpty("请输入消息内容", content);

View File

@ -0,0 +1,540 @@
package org.jeecg.modules.airag.llm.handler;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
import dev.langchain4j.service.tool.ToolExecutor;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.RestUtil;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
import java.util.*;
/**
* 插件工具构建器
* 根据插件配置构建ToolSpecification和ToolExecutor
* for [QQYUN-12453]【AI】支持插件
*
* @author chenrui
* @date 2025/10/30
*/
@Slf4j
public class PluginToolBuilder {
/**
* 从插件配置构建工具Map
*
* @param airagMcp 插件配置
* @return Map<ToolSpecification, ToolExecutor>
*/
public static Map<ToolSpecification, ToolExecutor> buildTools(AiragMcp airagMcp, HttpServletRequest currentHttpRequest) {
Map<ToolSpecification, ToolExecutor> tools = new HashMap<>();
if (airagMcp == null || oConvertUtils.isEmpty(airagMcp.getTools())) {
return tools;
}
try {
JSONArray toolsArray = JSONArray.parseArray(airagMcp.getTools());
if (toolsArray == null || toolsArray.isEmpty()) {
return tools;
}
String baseUrl = airagMcp.getEndpoint();
// 如果baseUrl为空使用当前系统地址
if (oConvertUtils.isEmpty(baseUrl)) {
if (currentHttpRequest != null) {
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
log.info("插件[{}]的BaseURL为空使用系统地址: {}", airagMcp.getName(), baseUrl);
} else {
log.warn("插件[{}]的BaseURL为空且无法获取系统地址跳过工具构建", airagMcp.getName());
return tools;
}
}
// 解析headers
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
// 解析并应用授权配置从metadata中读取
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
for (int i = 0; i < toolsArray.size(); i++) {
JSONObject toolConfig = toolsArray.getJSONObject(i);
if (toolConfig == null) {
continue;
}
try {
ToolSpecification spec = buildToolSpecification(toolConfig);
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap);
if (spec != null && executor != null) {
tools.put(spec, executor);
}
} catch (Exception e) {
log.error("构建插件工具失败,工具配置: {}", toolConfig.toJSONString(), e);
}
}
} catch (Exception e) {
log.error("解析插件工具配置失败,插件: {}", airagMcp.getName(), e);
}
return tools;
}
/**
* 构建ToolSpecification
*/
private static ToolSpecification buildToolSpecification(JSONObject toolConfig) {
String name = toolConfig.getString("name");
String description = toolConfig.getString("description");
if (oConvertUtils.isEmpty(name) || oConvertUtils.isEmpty(description)) {
log.warn("工具配置缺少name或description字段");
return null;
}
// 构建完整的描述信息(包含响应参数配置)
StringBuilder fullDescription = new StringBuilder(description);
// 解析响应参数并拼接到描述中
JSONArray responses = toolConfig.getJSONArray("responses");
if (responses != null && !responses.isEmpty()) {
fullDescription.append("\n\n返回值说明");
for (int i = 0; i < responses.size(); i++) {
JSONObject responseParam = responses.getJSONObject(i);
if (responseParam == null) {
continue;
}
String paramName = responseParam.getString("name");
String paramDesc = responseParam.getString("description");
String paramType = responseParam.getString("type");
if (oConvertUtils.isEmpty(paramName)) {
continue;
}
fullDescription.append("\n- ").append(paramName);
if (oConvertUtils.isNotEmpty(paramType)) {
fullDescription.append(" (").append(paramType).append(")");
}
if (oConvertUtils.isNotEmpty(paramDesc)) {
fullDescription.append(": ").append(paramDesc);
}
}
}
JsonObjectSchema.Builder schemaBuilder = JsonObjectSchema.builder();
// 解析请求参数
JSONArray parameters = toolConfig.getJSONArray("parameters");
if (parameters != null && !parameters.isEmpty()) {
List<String> requiredParams = new ArrayList<>();
for (int i = 0; i < parameters.size(); i++) {
JSONObject param = parameters.getJSONObject(i);
if (param == null) {
continue;
}
String paramName = param.getString("name");
String paramDesc = param.getString("description");
String paramType = param.getString("type");
if (oConvertUtils.isEmpty(paramName)) {
continue;
}
// 根据参数类型添加属性
if ("String".equalsIgnoreCase(paramType) || "string".equalsIgnoreCase(paramType)) {
schemaBuilder.addStringProperty(paramName, paramDesc != null ? paramDesc : "");
} else if ("Number".equalsIgnoreCase(paramType) || "number".equalsIgnoreCase(paramType)
|| "Integer".equalsIgnoreCase(paramType) || "integer".equalsIgnoreCase(paramType)) {
schemaBuilder.addNumberProperty(paramName, paramDesc != null ? paramDesc : "");
} else if ("Boolean".equalsIgnoreCase(paramType) || "boolean".equalsIgnoreCase(paramType)) {
schemaBuilder.addBooleanProperty(paramName, paramDesc != null ? paramDesc : "");
} else {
// 默认作为String处理
schemaBuilder.addStringProperty(paramName, paramDesc != null ? paramDesc : "");
}
// 检查是否必须
Boolean required = param.getBooleanValue("required");
if (required != null && required) {
requiredParams.add(paramName);
}
}
if (!requiredParams.isEmpty()) {
schemaBuilder.required(requiredParams.toArray(new String[0]));
}
}
return ToolSpecification.builder()
.name(name)
.description(fullDescription.toString())
.parameters(schemaBuilder.build())
.build();
}
/**
* 构建ToolExecutor
*/
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders) {
String path = toolConfig.getString("path");
String method = toolConfig.getString("method");
JSONArray parameters = toolConfig.getJSONArray("parameters");
if (oConvertUtils.isEmpty(path) || oConvertUtils.isEmpty(method)) {
log.warn("工具配置缺少path或method字段");
return null;
}
return (toolExecutionRequest, memoryId) -> {
try {
// 解析AI传入的参数
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
// 构建完整URL
String url = buildUrl(baseUrl, path, parameters, args);
// 构建请求方法
HttpMethod httpMethod = parseHttpMethod(method);
// 构建请求头
HttpHeaders httpHeaders = buildHttpHeaders(parameters, args, defaultHeaders);
// 构建请求参数
JSONObject urlVariables = buildUrlVariables(parameters, args);
Object body = buildRequestBody(parameters, args, httpHeaders);
// 发送HTTP请求
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class);
// 直接返回原始响应字符串,不进行解析
return response.getBody() != null ? response.getBody() : "";
} catch (HttpClientErrorException e) {
log.error("插件工具HTTP请求失败: {}", e.getMessage(), e);
return "请求失败: " + e.getStatusCode() + " - " + e.getResponseBodyAsString();
} catch (Exception e) {
log.error("插件工具执行失败: {}", e.getMessage(), e);
return "工具执行失败: " + e.getMessage();
}
};
}
/**
* 构建完整URL处理Path参数
*/
private static String buildUrl(String baseUrl, String path, JSONArray parameters, JSONObject args) {
String fullPath = path;
if (!path.startsWith("/")) {
fullPath = "/" + path;
}
// 拼接URL时防止出现双斜杠
if (baseUrl.endsWith("/") && fullPath.startsWith("/")) {
fullPath = fullPath.substring(1);
}
String url = baseUrl + fullPath;
// 替换Path参数
if (parameters != null && args != null) {
for (int i = 0; i < parameters.size(); i++) {
JSONObject param = parameters.getJSONObject(i);
if (param == null) {
continue;
}
String paramName = param.getString("name");
String paramLocation = param.getString("location");
if (!"Path".equalsIgnoreCase(paramLocation)) {
continue;
}
Object value = args.get(paramName);
if (value != null) {
url = url.replace("{" + paramName + "}", value.toString());
}
}
}
return url;
}
/**
* 构建请求头
*/
private static HttpHeaders buildHttpHeaders(JSONArray parameters, JSONObject args, Map<String, String> defaultHeaders) {
HttpHeaders httpHeaders = new HttpHeaders();
// 添加默认请求头
if (defaultHeaders != null) {
defaultHeaders.forEach(httpHeaders::set);
}
// 添加Header类型的参数
if (parameters != null && args != null) {
for (int i = 0; i < parameters.size(); i++) {
JSONObject param = parameters.getJSONObject(i);
if (param == null) {
continue;
}
String paramName = param.getString("name");
String paramLocation = param.getString("location");
if (!"Header".equalsIgnoreCase(paramLocation)) {
continue;
}
Object value = args.get(paramName);
if (value != null) {
httpHeaders.set(paramName, value.toString());
}
}
}
// 如果请求体不为空且没有设置Content-Type默认设置为application/json
if (!httpHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
}
return httpHeaders;
}
/**
* 构建URL查询参数
*/
private static JSONObject buildUrlVariables(JSONArray parameters, JSONObject args) {
JSONObject urlVariables = new JSONObject();
if (parameters == null || args == null) {
return urlVariables;
}
for (int i = 0; i < parameters.size(); i++) {
JSONObject param = parameters.getJSONObject(i);
if (param == null) {
continue;
}
String paramName = param.getString("name");
String paramLocation = param.getString("location");
String location = paramLocation != null ? paramLocation : "";
// 显式指定Query类型或者未指定类型默认作为Query
boolean isQueryParam = "Query".equalsIgnoreCase(location);
boolean isOtherType = "Body".equalsIgnoreCase(location) || "Form-Data".equalsIgnoreCase(location)
|| "Header".equalsIgnoreCase(location) || "Path".equalsIgnoreCase(location);
if (isQueryParam || !isOtherType) {
Object value = args.get(paramName);
if (value != null) {
urlVariables.put(paramName, value);
}
}
}
return urlVariables.isEmpty() ? null : urlVariables;
}
/**
* 构建请求体
*/
private static Object buildRequestBody(JSONArray parameters, JSONObject args, HttpHeaders httpHeaders) {
if (parameters == null || args == null) {
return null;
}
boolean hasBody = false;
boolean hasFormData = false;
// 检查是否有Body或Form-Data类型的参数
for (int i = 0; i < parameters.size(); i++) {
JSONObject param = parameters.getJSONObject(i);
if (param == null) {
continue;
}
String paramLocation = param.getString("location");
if ("Body".equalsIgnoreCase(paramLocation)) {
hasBody = true;
} else if ("Form-Data".equalsIgnoreCase(paramLocation)) {
hasFormData = true;
}
}
// Body和Form-Data互斥
if (hasBody && hasFormData) {
log.warn("工具配置同时包含Body和Form-Data类型参数优先使用Body");
hasFormData = false;
}
if (hasBody) {
// Body类型构建JSON对象
JSONObject body = new JSONObject();
for (int i = 0; i < parameters.size(); i++) {
JSONObject param = parameters.getJSONObject(i);
if (param == null) {
continue;
}
String paramName = param.getString("name");
String paramLocation = param.getString("location");
if (!"Body".equalsIgnoreCase(paramLocation) ) {
continue;
}
Object value = args.get(paramName);
if (value != null) {
body.put(paramName, value);
} else {
// 检查是否有默认值
String defaultValue = param.getString("defaultValue");
if (oConvertUtils.isNotEmpty(defaultValue)) {
body.put(paramName, defaultValue);
}
}
}
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
return body.isEmpty() ? null : body;
} else if (hasFormData) {
// Form-Data类型构建JSON对象RestUtil会处理
JSONObject formData = new JSONObject();
for (int i = 0; i < parameters.size(); i++) {
JSONObject param = parameters.getJSONObject(i);
if (param == null) {
continue;
}
String paramName = param.getString("name");
String paramLocation = param.getString("location");
if (!"Form-Data".equalsIgnoreCase(paramLocation)) {
continue;
}
Object value = args.get(paramName);
if (value != null) {
formData.put(paramName, value);
} else {
String defaultValue = param.getString("defaultValue");
if (oConvertUtils.isNotEmpty(defaultValue)) {
formData.put(paramName, defaultValue);
}
}
}
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
return formData.isEmpty() ? null : formData;
}
return null;
}
/**
* 解析HTTP方法
*/
private static HttpMethod parseHttpMethod(String method) {
try {
return HttpMethod.valueOf(method.toUpperCase());
} catch (IllegalArgumentException e) {
log.warn("无效的HTTP方法: {}使用默认GET", method);
return HttpMethod.GET;
}
}
/**
* 解析headers JSON字符串为Map
*/
private static Map<String, String> parseHeaders(String headersStr) {
Map<String, String> headersMap = new HashMap<>();
if (oConvertUtils.isEmpty(headersStr)) {
return headersMap;
}
try {
JSONObject headersJson = JSONObject.parseObject(headersStr);
if (headersJson != null) {
headersJson.forEach((key, value) -> {
if (value != null) {
headersMap.put(key, value.toString());
}
});
}
} catch (Exception e) {
log.warn("解析headers失败: {}", headersStr);
}
return headersMap;
}
/**
* 应用授权配置到headers
* 从metadata中读取授权配置如果是Token授权添加到headers中
* 如果授权类型为token但没有设置token值则从TokenUtils获取当前请求的token
*
* @param headersMap 请求头Map
* @param metadataStr 元数据JSON字符串
*/
private static void applyAuthConfig(Map<String, String> headersMap, String metadataStr, HttpServletRequest currentHttpRequest) {
if (oConvertUtils.isEmpty(metadataStr)) {
return;
}
try {
JSONObject metadata = JSONObject.parseObject(metadataStr);
if (metadata == null) {
return;
}
String authType = metadata.getString("authType");
if (oConvertUtils.isEmpty(authType) || !"token".equalsIgnoreCase(authType)) {
return;
}
// Token授权方式从metadata中获取token配置并添加到headers
String tokenParamName = metadata.getString("tokenParamName");
String tokenParamValue = metadata.getString("tokenParamValue");
// 如果token参数名存在但token值未设置尝试从TokenUtils获取当前请求的token
if (oConvertUtils.isNotEmpty(tokenParamName) && oConvertUtils.isEmpty(tokenParamValue)) {
try {
// 注意TokenUtils需要获取当前线程的request所以必须在同步调用中使用
String currentToken = TokenUtils.getTokenByRequest();
if(oConvertUtils.isEmpty(currentToken) && currentHttpRequest != null) {
currentToken = TokenUtils.getTokenByRequest(currentHttpRequest);
}
if (oConvertUtils.isNotEmpty(currentToken)) {
tokenParamValue = currentToken;
log.debug("从TokenUtils获取Token并添加到请求头: {} = {}", tokenParamName,
currentToken.length() > 10 ? currentToken.substring(0, 10) + "..." : currentToken);
} else {
log.warn("Token授权配置中tokenParamValue为空且无法从TokenUtils获取当前请求的token");
}
} catch (Exception e) {
log.warn("从TokenUtils获取token失败: {}", e.getMessage());
}
}
if (oConvertUtils.isNotEmpty(tokenParamName) && oConvertUtils.isNotEmpty(tokenParamValue)) {
// 如果headers中已存在同名header优先使用metadata中的配置覆盖
headersMap.put(tokenParamName, tokenParamValue);
// 日志中只显示token的前几个字符避免泄露完整token
String tokenPreview = tokenParamValue.length() > 10
? tokenParamValue.substring(0, 10) + "..."
: tokenParamValue;
log.debug("添加Token授权到请求头: {} = {}", tokenParamName, tokenPreview);
} else {
log.warn("Token授权配置不完整: tokenParamName={}, tokenParamValue={}", tokenParamName, tokenParamValue != null ? "***" : null);
}
} catch (Exception e) {
log.warn("解析授权配置失败: {}", metadataStr, e);
}
}
}

View File

@ -0,0 +1,14 @@
package org.jeecg.modules.airag.llm.mapper;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Description: MCP
* @Author: jeecg-boot
* @Date: 2025-10-20
* @Version: V1.0
*/
public interface AiragMcpMapper extends BaseMapper<AiragMcp> {
}

View File

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

View File

@ -0,0 +1,32 @@
package org.jeecg.modules.airag.llm.service;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @Description: MCP
* @Author: jeecg-boot
* @Date: 2025-10-20
* @Version: V1.0
*/
public interface IAiragMcpService extends IService<AiragMcp> {
Result<String> edit(AiragMcp airagMcp);
Result<?> sync(String id);
Result<?> toggleStatus(String id, String action);
/**
* 保存插件工具仅更新tools字段
* for [QQYUN-12453]【AI】支持插件
* @param id 插件ID
* @param tools 工具列表JSON字符串
* @return 操作结果
* @author chenrui
* @date 2025/10/30
*/
Result<String> saveTools(String id, String tools);
}

View File

@ -0,0 +1,356 @@
package org.jeecg.modules.airag.llm.service.impl;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
import dev.langchain4j.model.chat.request.json.*;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
import org.jeecg.modules.airag.llm.service.IAiragMcpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Description: MCP
* @Author: jeecg-boot
* @Date: 2025-10-20
* @Version: V1.0
*/
@Service("airagMcpServiceImpl")
@Slf4j
public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService {
@Autowired
private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper
/**
* 新增或编辑Mcpserver
*
* @param airagMcp MCP对象
* @return 返回保存后的MCP对象
* @author chenrui
* @date 2025/10/21
*/
@Override
public Result<String> edit(AiragMcp airagMcp) {
// 校验必填项
if (airagMcp.getName() == null || airagMcp.getName().trim().isEmpty()) {
return Result.error("名称不能为空");
}
//update-begin---author:chenrui ---date:20251031 for[QQYUN-12453]【AI】支持插件------------
// 设置默认category
if (oConvertUtils.isEmpty(airagMcp.getCategory())) {
airagMcp.setCategory("mcp");
}
// 对于MCP类型需要校验type和endpoint
if ("mcp".equalsIgnoreCase(airagMcp.getCategory())) {
if (airagMcp.getType() == null || airagMcp.getType().trim().isEmpty()) {
return Result.error("MCP类型不能为空");
}
if (airagMcp.getEndpoint() == null || airagMcp.getEndpoint().trim().isEmpty()) {
return Result.error("服务端点不能为空");
}
} else if ("plugin".equalsIgnoreCase(airagMcp.getCategory())) {
// 对于插件类型BaseURL可选不填时使用当前系统地址
// 不再校验endpoint是否为空
} else {
// 未知类型默认为MCP并校验
if (airagMcp.getEndpoint() == null || airagMcp.getEndpoint().trim().isEmpty()) {
return Result.error("服务端点不能为空");
}
}
//update-end---author:chenrui ---date:20251031 for[QQYUN-12453]【AI】支持插件------------
if (airagMcp.getId() == null || airagMcp.getId().trim().isEmpty()) {
// 设置默认值
airagMcp.setStatus("enable");
//update-begin---author:chenrui ---date:20251031 for[QQYUN-12453]【AI】支持插件------------
// 只有MCP类型才设置synced字段插件类型不需要同步默认为已同步
if ("mcp".equalsIgnoreCase(airagMcp.getCategory())) {
airagMcp.setSynced(CommonConstant.STATUS_0_INT);
} else {
airagMcp.setSynced(CommonConstant.STATUS_1_INT);
}
//update-end---author:chenrui ---date:20251031 for[QQYUN-12453]【AI】支持插件------------
// 新增
this.save(airagMcp);
} else {
// 编辑
this.updateById(airagMcp);
}
return Result.OK("保存成功");
}
/**
* 同步mcp的工具列表
*
* @param id mcp主键
* @return 工具列表
* @author chenrui
* @date 2025/10/21
*/
@Override
public Result<?> sync(String id) {
AiragMcp mcp = this.getById(id);
if (mcp == null) {
return Result.error("未找到对应的MCP对象");
}
//update-begin---author:chenrui ---date:20251031 for[QQYUN-12453]【AI】支持插件------------
// 只有MCP类型才支持同步插件类型不支持
String category = mcp.getCategory();
if (oConvertUtils.isEmpty(category)) {
category = "mcp"; // 兼容旧数据
}
if (!"mcp".equalsIgnoreCase(category)) {
return Result.error("只有MCP类型才支持同步操作");
}
//update-end---author:chenrui ---date:20251031 for[QQYUN-12453]【AI】支持插件------------
String type = mcp.getType();
String endpoint = mcp.getEndpoint();
Map<String, String> headers = null;
if (oConvertUtils.isNotEmpty(mcp.getHeaders())) {
try {
headers = JSONObject.parseObject(mcp.getHeaders(), Map.class);
} catch (JSONException e) {
headers = null;
}
}
if (type == null || endpoint == null) {
return Result.error("MCP类型或端点为空");
}
McpClient mcpClient = null;
try {
if ("sse".equalsIgnoreCase(type)) {
HttpMcpTransport.Builder builder = new HttpMcpTransport.Builder()
.sseUrl(endpoint)
.logRequests(true)
.logResponses(true);
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
} else if ("stdio".equalsIgnoreCase(type)) {
// stdio 类型endpoint 可能是一个命令行,需要拆分为命令列表
// List<String> cmdParts = Arrays.asList(endpoint.trim().split("\\s+"));
// StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
// .command(cmdParts)
// .environment(headers);
// mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
return Result.error("不支持的MCP类型:" + type);
} else {
return Result.error("不支持的MCP类型:" + type);
}
List<ToolSpecification> toolSpecifications = mcpClient.listTools();
// 先尝试直接使用 ObjectMapper 序列化,若结果为 {} 则回退到反射 Map
List<Map<String, Object>> specMaps = toolSpecifications.stream()
.map(spec -> {
try {
String raw = objectMapper.writeValueAsString(spec);
if (raw != null && raw.length() > 2) {
// 直接反序列化成 Map保留 Jackson 认出的字段
return objectMapper.readValue(raw, new TypeReference<Map<String, Object>>() {
});
}
} catch (Exception ignore) {
}
return convertToolSpec(spec);
})
.collect(Collectors.toList());
String jsonList;
try {
jsonList = objectMapper.writeValueAsString(specMaps);
} catch (JsonProcessingException e) {
jsonList = JSONObject.toJSONString(specMaps);
}
String firstJson = specMaps.isEmpty() ? "null" : safeWriteJson(specMaps.get(0));
log.info("MCP工具列表 id={}, size={}, first={}", id, toolSpecifications.size(), firstJson);
mcp.setTools(jsonList);
mcp.setSynced(1);
Map<String,Object> metadata = new HashMap<>();
metadata.put("tool_count", toolSpecifications.size());
mcp.setMetadata(objectMapper.writeValueAsString(metadata));
this.updateById(mcp);
return Result.OK(specMaps);
} catch (Exception e) {
String message = e.getMessage();
if (e instanceof IllegalArgumentException) {
message = "MCP客户端参数错误";
}
log.error("同步MCP工具失败 id={}, error={}", id, message, e);
return Result.error("同步失败" + message);
} finally {
if (mcpClient != null) {
try {
Method closeMethod = mcpClient.getClass().getMethod("close");
closeMethod.invoke(mcpClient);
} catch (NoSuchMethodException ignore) {
} catch (Exception ex) {
log.warn("关闭MCP客户端失败 id={}, error={}", id, ex.getMessage());
}
}
}
}
// 安全序列化单个对象为 JSON 字符串
private String safeWriteJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
return String.valueOf(obj);
}
}
// 反射将 ToolSpecification 转成 Map兼容 record/私有字段/仅 Jackson 注解场景 -> 改为直接调用访问器
private Map<String, Object> convertToolSpec(ToolSpecification spec) {
Map<String, Object> map = new LinkedHashMap<>();
if (spec == null) {
return map;
}
map.put("name", spec.name());
map.put("description", spec.description());
try {
Object params = spec.parameters();
if (params != null) {
JsonObjectSchema obj = (JsonObjectSchema) params;
List<Map<String, Object>> fields = new ArrayList<>();
if (obj.properties() != null) {
obj.properties().forEach((fieldName, fieldSchema) -> {
Map<String, Object> fieldMap = new LinkedHashMap<>();
fieldMap.put("name", fieldName);
fieldMap.put("description", extractDescription(fieldSchema));
// 若需要标记必填
if (obj.required() != null && obj.required().contains(fieldName)) {
fieldMap.put("required", true);
}
fields.add(fieldMap);
});
}
map.put("parameters", fields);
}
} catch (Exception ignored) {
}
return map;
}
// 提取各类型 schema 的描述
private String extractDescription(Object schema) {
if (schema == null) return null;
try {
if (schema instanceof JsonStringSchema) return ((JsonStringSchema) schema).description();
if (schema instanceof JsonNumberSchema) return ((JsonNumberSchema) schema).description();
if (schema instanceof JsonBooleanSchema) return ((JsonBooleanSchema) schema).description();
if (schema instanceof JsonArraySchema) return ((JsonArraySchema) schema).description();
if (schema instanceof JsonEnumSchema) return ((JsonEnumSchema) schema).description();
if (schema instanceof JsonObjectSchema) return ((JsonObjectSchema) schema).description();
} catch (Exception ignored) {
}
return schema.toString();
}
/**
* 修改状态
*
* @param id MCP主键
* @param action 操作enable/disable
* @return 操作结果
* @author chenrui
* @date 2025/10/21 11:00
*/
@Override
public Result<?> toggleStatus(String id, String action) {
if (oConvertUtils.isEmpty(id)) {
return Result.error("id不能为空");
}
if (oConvertUtils.isEmpty(action)) {
return Result.error("action不能为空");
}
String normalized = action.toLowerCase();
if (!"enable".equals(normalized) && !"disable".equals(normalized)) {
return Result.error("action只能为enable或disable");
}
AiragMcp mcp = this.getById(id);
if (mcp == null) {
return Result.error("未找到对应的MCP服务");
}
if (normalized.equalsIgnoreCase(mcp.getStatus())) {
return Result.OK("操作成功");
}
mcp.setStatus(normalized);
this.updateById(mcp);
return Result.OK("操作成功");
}
/**
* 保存插件工具仅更新tools字段
* for [QQYUN-12453]【AI】支持插件
* @param id 插件ID
* @param tools 工具列表JSON字符串
* @return 操作结果
* @author chenrui
* @date 2025/10/30
*/
@Override
public Result<String> saveTools(String id, String tools) {
if (oConvertUtils.isEmpty(id)) {
return Result.error("插件ID不能为空");
}
AiragMcp mcp = this.getById(id);
if (mcp == null) {
return Result.error("未找到对应的插件");
}
// 验证是否为插件类型
String category = mcp.getCategory();
if (oConvertUtils.isEmpty(category)) {
category = "mcp"; // 兼容旧数据
}
if (!"plugin".equalsIgnoreCase(category)) {
return Result.error("只有插件类型才能保存工具");
}
// 更新tools字段
mcp.setTools(tools);
// 更新metadata中的tool_count
try {
com.alibaba.fastjson.JSONArray toolsArray = com.alibaba.fastjson.JSONArray.parseArray(tools);
int toolCount = toolsArray != null ? toolsArray.size() : 0;
// 解析现有metadata
JSONObject metadata = new JSONObject();
if (oConvertUtils.isNotEmpty(mcp.getMetadata())) {
try {
JSONObject metadataJson = JSONObject.parseObject(mcp.getMetadata());
if (metadataJson != null) {
metadata.putAll(metadataJson);
}
} catch (Exception e) {
log.warn("解析metadata失败将重新创建: {}", mcp.getMetadata());
}
}
// 更新tool_count
metadata.put("tool_count", toolCount);
// 保存metadata
mcp.setMetadata(metadata.toJSONString());
} catch (Exception e) {
log.warn("更新工具数量失败: {}", e.getMessage());
// 即使更新tool_count失败也不影响保存tools
}
this.updateById(mcp);
return Result.OK("保存成功");
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>jeecg-boot-module</artifactId>
<groupId>org.jeecgframework.boot3</groupId>
<version>3.8.3</version>
<version>3.9.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -11,7 +11,7 @@
//import org.springframework.web.bind.annotation.GetMapping;
//import org.springframework.web.bind.annotation.RequestMapping;
//import org.springframework.web.bind.annotation.RestController;
//import javax.annotation.Resource;
//import jakarta.annotation.Resource;
//import java.util.List;
//
///**

View File

@ -0,0 +1,333 @@
package org.jeecg.modules.demo.shop.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.demo.shop.entity.Order;
import org.jeecg.modules.demo.shop.entity.Product;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* 商品管理模拟接口
* 用于AI Agent通过工具帮助用户查询商品并采购的业务演示
* @Author: chenrui
* @Date: 2025-11-06
*/
@Tag(name = "商品管理Demo")
@RestController
@RequestMapping("/demo/shop")
@Slf4j
public class ShopController {
/**
* 商品数据存储(内存)
*/
private final Map<String, Product> productStore = new ConcurrentHashMap<>();
/**
* 订单数据存储(内存)
*/
private final Map<String, Order> orderStore = new ConcurrentHashMap<>();
/**
* 订单ID生成器
*/
private final AtomicInteger orderIdGenerator = new AtomicInteger(1000);
/**
* 初始化商品数据
*
* @author chenrui
* @date 2025/11/6 14:30
*/
@PostConstruct
public void initProducts() {
// 电子产品
productStore.put("P001", new Product("P001", "iPhone 15 Pro", new BigDecimal("7999.00"), "电子产品", "Apple最新旗舰手机,6.1英寸屏幕,钛金属边框", 50));
productStore.put("P002", new Product("P002", "MacBook Pro 14", new BigDecimal("14999.00"), "电子产品", "M3 Pro芯片,16GB内存,512GB存储", 30));
productStore.put("P003", new Product("P003", "AirPods Pro 2", new BigDecimal("1899.00"), "电子产品", "主动降噪无线耳机,支持空间音频", 100));
productStore.put("P004", new Product("P004", "iPad Air", new BigDecimal("4799.00"), "电子产品", "10.9英寸液晶显示屏,M1芯片", 60));
// 图书
productStore.put("B001", new Product("B001", "Java核心技术卷I", new BigDecimal("119.00"), "图书", "Java编程经典教材,适合初学者和进阶开发者", 200));
productStore.put("B002", new Product("B002", "深入理解计算机系统", new BigDecimal("139.00"), "图书", "CSAPP经典教材,计算机系统必读书籍", 150));
productStore.put("B003", new Product("B003", "设计模式", new BigDecimal("89.00"), "图书", "软件设计经典著作,GoF四人组著作", 180));
// 生活用品
productStore.put("L001", new Product("L001", "小米电动牙刷", new BigDecimal("199.00"), "生活用品", "声波震动,IPX7防水,续航18天", 300));
productStore.put("L002", new Product("L002", "戴森吹风机", new BigDecimal("2990.00"), "生活用品", "快速干发,智能温控,保护头发", 80));
productStore.put("L003", new Product("L003", "膳魔师保温杯", new BigDecimal("259.00"), "生活用品", "真空保温,304不锈钢,保温12小时", 500));
// 食品
productStore.put("F001", new Product("F001", "三只松鼠坚果礼盒", new BigDecimal("159.00"), "食品", "混合坚果大礼包,1500g装", 400));
productStore.put("F002", new Product("F002", "茅台飞天53度", new BigDecimal("2899.00"), "食品", "贵州茅台酒,500ml", 20));
productStore.put("F003", new Product("F003", "星巴克咖啡豆", new BigDecimal("128.00"), "食品", "中度烘焙,派克市场,250g", 250));
log.info("商品数据初始化完成,共{}个商品", productStore.size());
}
/**
* 查询商品列表
*
* @param category 商品分类(可选)
* @param keyword 搜索关键词(可选)
* @return 商品列表
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "查询商品列表", description = "支持按分类和关键词搜索")
@GetMapping("/products")
public Result<List<Product>> getProducts(
@Parameter(description = "商品分类") @RequestParam(required = false) String category,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword) {
log.info("查询商品列表 - 分类: {}, 关键词: {}", category, keyword);
List<Product> products = new ArrayList<>(productStore.values());
// 按分类过滤
if (category != null && !category.trim().isEmpty()) {
products = products.stream()
.filter(p -> category.equals(p.getCategory()))
.collect(Collectors.toList());
}
// 按关键词过滤(搜索商品名称和描述)
if (keyword != null && !keyword.trim().isEmpty()) {
String searchKey = keyword.toLowerCase();
products = products.stream()
.filter(p -> p.getName().toLowerCase().contains(searchKey)
|| p.getDescription().toLowerCase().contains(searchKey))
.collect(Collectors.toList());
}
// 按价格排序
products.sort(Comparator.comparing(Product::getPrice));
log.info("查询到{}个商品", products.size());
return Result.OK(products);
}
/**
* 查询商品库存
*
* @param productId 商品ID
* @return 库存信息
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "查询商品库存", description = "根据商品ID查询库存数量")
@GetMapping("/stock")
public Result<Map<String, Object>> getStock(
@Parameter(description = "商品ID", required = true) @RequestParam String productId) {
log.info("查询商品库存 - 商品ID: {}", productId);
Product product = productStore.get(productId);
if (product == null) {
return Result.error("商品不存在: " + productId);
}
Map<String, Object> stockInfo = new HashMap<>();
stockInfo.put("productId", product.getId());
stockInfo.put("productName", product.getName());
stockInfo.put("stock", product.getStock());
stockInfo.put("available", product.getStock() > 0);
return Result.OK(stockInfo);
}
/**
* 购买商品(下单)
*
* @param productId 商品ID
* @param quantity 购买数量
* @param userId 用户ID(可选)
* @return 订单信息
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "购买商品", description = "创建订单,但不立即扣减库存")
@PostMapping("/purchase")
public Result<Order> purchase(
@Parameter(description = "商品ID", required = true) @RequestParam String productId,
@Parameter(description = "购买数量", required = true) @RequestParam Integer quantity,
@Parameter(description = "用户ID") @RequestParam(required = false) String userId) {
log.info("购买商品 - 商品ID: {}, 数量: {}, 用户: {}", productId, quantity, userId);
// 参数校验
if (quantity == null || quantity <= 0) {
return Result.error("购买数量必须大于0");
}
// 查询商品
Product product = productStore.get(productId);
if (product == null) {
return Result.error("商品不存在: " + productId);
}
// 检查库存
if (product.getStock() < quantity) {
return Result.error("库存不足,当前库存: " + product.getStock());
}
// 创建订单
String orderId = "O" + orderIdGenerator.incrementAndGet();
BigDecimal totalAmount = product.getPrice().multiply(new BigDecimal(quantity));
Order order = new Order();
order.setId(orderId);
order.setProductId(productId);
order.setProductName(product.getName());
order.setQuantity(quantity);
order.setUnitPrice(product.getPrice());
order.setTotalAmount(totalAmount);
order.setStatus("pending");
order.setCreateTime(new Date());
order.setUserId(userId);
orderStore.put(orderId, order);
log.info("订单创建成功 - 订单ID: {}, 总金额: {}", orderId, totalAmount);
return Result.OK(order);
}
/**
* 扣减商品库存
*
* @param orderId 订单ID
* @return 扣减结果
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "扣减商品库存", description = "根据订单ID扣减对应商品库存")
@PostMapping("/stock/deduct")
public Result<Map<String, Object>> deductStock(
@Parameter(description = "订单ID", required = true) @RequestParam String orderId) {
log.info("扣减库存 - 订单ID: {}", orderId);
// 查询订单
Order order = orderStore.get(orderId);
if (order == null) {
return Result.error("订单不存在: " + orderId);
}
// 检查订单状态
if ("paid".equals(order.getStatus())) {
return Result.error("订单已支付,库存已扣减");
}
if ("cancelled".equals(order.getStatus())) {
return Result.error("订单已取消");
}
// 查询商品
Product product = productStore.get(order.getProductId());
if (product == null) {
return Result.error("商品不存在: " + order.getProductId());
}
// 检查库存
synchronized (product) {
if (product.getStock() < order.getQuantity()) {
return Result.error("库存不足,当前库存: " + product.getStock() + ", 需要: " + order.getQuantity());
}
// 扣减库存
int newStock = product.getStock() - order.getQuantity();
product.setStock(newStock);
// 更新订单状态
order.setStatus("paid");
log.info("库存扣减成功 - 商品: {}, 扣减数量: {}, 剩余库存: {}",
product.getName(), order.getQuantity(), newStock);
}
Map<String, Object> result = new HashMap<>();
result.put("orderId", orderId);
result.put("productId", product.getId());
result.put("productName", product.getName());
result.put("deductedQuantity", order.getQuantity());
result.put("remainingStock", product.getStock());
result.put("orderStatus", order.getStatus());
return Result.OK(result);
}
/**
* 查询订单详情
*
* @param orderId 订单ID
* @return 订单详情
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "查询订单详情", description = "根据订单ID查询订单信息")
@GetMapping("/order")
public Result<Order> getOrder(
@Parameter(description = "订单ID", required = true) @RequestParam String orderId) {
log.info("查询订单 - 订单ID: {}", orderId);
Order order = orderStore.get(orderId);
if (order == null) {
return Result.error("订单不存在: " + orderId);
}
return Result.OK(order);
}
/**
* 获取所有商品分类
*
* @return 分类列表
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "获取商品分类", description = "获取所有商品的分类列表")
@GetMapping("/categories")
public Result<List<String>> getCategories() {
Set<String> categories = productStore.values().stream()
.map(Product::getCategory)
.collect(Collectors.toSet());
List<String> categoryList = new ArrayList<>(categories);
categoryList.sort(String::compareTo);
return Result.OK(categoryList);
}
/**
* 重置所有数据(仅用于测试)
*
* @return 重置结果
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "重置数据", description = "清空所有订单并重置商品库存(仅用于测试)")
@PostMapping("/reset")
public Result<String> reset() {
log.info("重置商品和订单数据");
orderStore.clear();
orderIdGenerator.set(1000);
productStore.clear();
initProducts();
return Result.OK("数据重置成功");
}
}

View File

@ -0,0 +1,64 @@
package org.jeecg.modules.demo.shop.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
/**
* 订单实体
* @Author: chenrui
* @Date: 2025-11-06
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
/**
* 订单ID
*/
private String id;
/**
* 商品ID
*/
private String productId;
/**
* 商品名称
*/
private String productName;
/**
* 购买数量
*/
private Integer quantity;
/**
* 单价
*/
private BigDecimal unitPrice;
/**
* 总金额
*/
private BigDecimal totalAmount;
/**
* 订单状态: pending-待支付, paid-已支付, cancelled-已取消
*/
private String status;
/**
* 创建时间
*/
private Date createTime;
/**
* 用户信息(可选)
*/
private String userId;
}

View File

@ -0,0 +1,48 @@
package org.jeecg.modules.demo.shop.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 商品实体
* @Author: chenrui
* @Date: 2025-11-06
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
/**
* 商品ID
*/
private String id;
/**
* 商品名称
*/
private String name;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 商品分类
*/
private String category;
/**
* 商品描述
*/
private String description;
/**
* 库存数量
*/
private Integer stock;
}

View File

@ -1,438 +0,0 @@
package org.jeecg.modules.dlglong.controller;
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.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.MatchTypeEnum;
import org.jeecg.common.system.query.QueryCondition;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.constant.VxeSocketConst;
import org.jeecg.modules.demo.mock.vxe.websocket.VxeSocket;
import org.jeecg.modules.dlglong.entity.MockEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.util.*;
/**
* @Description: DlMockController
* @author: jeecg-boot
*/
@Slf4j
@RestController
@RequestMapping("/mock/dlglong")
public class DlMockController {
/**
* 模拟更改状态
*
* @param id
* @param status
* @return
*/
@GetMapping("/change1")
public Result mockChange1(@RequestParam("id") String id, @RequestParam("status") String status) {
/* id 为 行的idrowId只要获取到rowId那么只需要调用 VXESocket.sendMessageToAll() 即可 */
// 封装行数据
JSONObject rowData = new JSONObject();
// 这个字段就是要更改的行数据ID
rowData.put("id", id);
// 这个字段就是要更改的列的key和具体的值
rowData.put("status", status);
// 模拟更改数据
this.mockChange(rowData);
return Result.ok();
}
/**
* 模拟更改拖轮状态
*
* @param id
* @param tugStatus
* @return
*/
@GetMapping("/change2")
public Result mockChange2(@RequestParam("id") String id, @RequestParam("tug_status") String tugStatus) {
/* id 为 行的idrowId只要获取到rowId那么只需要调用 VXESocket.sendMessageToAll() 即可 */
// 封装行数据
JSONObject rowData = new JSONObject();
// 这个字段就是要更改的行数据ID
rowData.put("id", id);
// 这个字段就是要更改的列的key和具体的值
JSONObject status = JSON.parseObject(tugStatus);
rowData.put("tug_status", status);
// 模拟更改数据
this.mockChange(rowData);
return Result.ok();
}
/**
* 模拟更改进度条状态
*
* @param id
* @param progress
* @return
*/
@GetMapping("/change3")
public Result mockChange3(@RequestParam("id") String id, @RequestParam("progress") String progress) {
/* id 为 行的idrowId只要获取到rowId那么只需要调用 VXESocket.sendMessageToAll() 即可 */
// 封装行数据
JSONObject rowData = new JSONObject();
// 这个字段就是要更改的行数据ID
rowData.put("id", id);
// 这个字段就是要更改的列的key和具体的值
rowData.put("progress", progress);
// 模拟更改数据
this.mockChange(rowData);
return Result.ok();
}
private void mockChange(JSONObject rowData) {
// 封装socket数据
JSONObject socketData = new JSONObject();
// 这里的 socketKey 必须要和调度计划页面上写的 socketKey 属性保持一致
socketData.put("socketKey", "page-dispatch");
// 这里的 args 必须得是一个数组下标0是行数据下标1是caseId一般不用传
socketData.put("args", new Object[]{rowData, ""});
// 封装消息字符串,这里的 type 必须是 VXESocketConst.TYPE_UVT
String message = VxeSocket.packageMessage(VxeSocketConst.TYPE_UVT, socketData);
// 调用 sendMessageToAll 发送给所有在线的用户
VxeSocket.sendMessageToAll(message);
}
/**
* 模拟更改【大船待审】状态
*
* @param status
* @return
*/
@GetMapping("/change4")
public Result mockChange4(@RequestParam("status") String status) {
// 封装socket数据
JSONObject socketData = new JSONObject();
// 这里的 key 是前端注册时使用的key必须保持一致
socketData.put("key", "dispatch-dcds-status");
// 这里的 args 必须得是一个数组,每一位都是注册方法的参数,按顺序传递
socketData.put("args", new Object[]{status});
// 封装消息字符串,这里的 type 必须是 VXESocketConst.TYPE_UVT
String message = VxeSocket.packageMessage(VxeSocketConst.TYPE_CSD, socketData);
// 调用 sendMessageToAll 发送给所有在线的用户
VxeSocket.sendMessageToAll(message);
return Result.ok();
}
/**
* 【模拟】即时保存单行数据
*
* @param rowData 行数据,实际使用时可以替换成一个实体类
*/
@PutMapping("/immediateSaveRow")
public Result mockImmediateSaveRow(@RequestBody JSONObject rowData) throws Exception {
System.out.println("即时保存.rowData" + rowData.toJSONString());
// 延时1.5秒,模拟网慢堵塞真实感
Thread.sleep(500);
return Result.ok();
}
/**
* 【模拟】即时保存整个表格的数据
*
* @param tableData 表格数据实际使用时可以替换成一个List实体类
*/
@PostMapping("/immediateSaveAll")
public Result mockImmediateSaveAll(@RequestBody JSONArray tableData) throws Exception {
// 【注】:
// 1、tableData里包含该页所有的数据
// 2、如果你实现了“即时保存”那么除了新增的数据其他的都是已经保存过的了
// 不需要再进行一次update操作了所以可以在前端传数据的时候就遍历判断一下
// 只传新增的数据给后台insert即可否者将会造成性能上的浪费。
// 3、新增的行是没有id的通过这一点就可以判断是否是新增的数据
System.out.println("即时保存.tableData" + tableData.toJSONString());
// 延时1.5秒,模拟网慢堵塞真实感
Thread.sleep(1000);
return Result.ok();
}
/**
* 获取模拟数据
*
* @param pageNo 页码
* @param pageSize 页大小
* @param parentId 父ID不传则查询顶级
* @return
*/
@GetMapping("/getData")
public Result getMockData(
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
// 父级id根据父级id查询子级如果为空则查询顶级
@RequestParam(name = "parentId", required = false) String parentId
) {
// 模拟JSON数据路径
String path = "classpath:org/jeecg/modules/dlglong/json/dlglong.json";
// 读取JSON数据
JSONArray dataList = readJsonData(path);
if (dataList == null) {
return Result.error("读取数据失败!");
}
IPage<JSONObject> page = this.queryDataPage(dataList, parentId, pageNo, pageSize);
return Result.ok(page);
}
/**
* 获取模拟“调度计划”页面的数据
*
* @param pageNo 页码
* @param pageSize 页大小
* @param parentId 父ID不传则查询顶级
* @return
*/
@GetMapping("/getDdjhData")
public Result getMockDdjhData(
// SpringMVC 会自动将参数注入到实体里
MockEntity mockEntity,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
// 父级id根据父级id查询子级如果为空则查询顶级
@RequestParam(name = "parentId", required = false) String parentId,
@RequestParam(name = "status", required = false) String status,
// 高级查询条件
@RequestParam(name = "superQueryParams", required = false) String superQueryParams,
// 高级查询模式
@RequestParam(name = "superQueryMatchType", required = false) String superQueryMatchType,
HttpServletRequest request
) {
// 获取查询条件(前台传递的查询参数)
Map<String, String[]> parameterMap = request.getParameterMap();
// 遍历输出到控制台
System.out.println("\ngetDdjhData - 普通查询条件:");
for (String key : parameterMap.keySet()) {
System.out.println("-- " + key + ": " + JSON.toJSONString(parameterMap.get(key)));
}
// 输出高级查询
try {
System.out.println("\ngetDdjhData - 高级查询条件:");
// 高级查询模式
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
if (matchType == null) {
System.out.println("-- 高级查询模式:不识别(" + superQueryMatchType + "");
} else {
System.out.println("-- 高级查询模式:" + matchType.getValue());
}
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
if (conditions != null) {
for (QueryCondition condition : conditions) {
System.out.println("-- " + JSON.toJSONString(condition));
}
} else {
System.out.println("-- 没有传递任何高级查询条件");
}
System.out.println();
} catch (Exception e) {
log.error("-- 高级查询操作失败:" + superQueryParams, e);
e.printStackTrace();
}
/* 注:实际使用中不用写上面那种繁琐的代码,这里只是为了直观的输出到控制台里而写的示例,
使用下面这种写法更简洁方便 */
// 封装成 MyBatisPlus 能识别的 QueryWrapper可以直接使用这个对象进行SQL筛选条件拼接
// 这个方法也会自动封装高级查询条件但是高级查询参数名必须是superQueryParams和superQueryMatchType
QueryWrapper<MockEntity> queryWrapper = QueryGenerator.initQueryWrapper(mockEntity, parameterMap);
System.out.println("queryWrapper " + queryWrapper.getCustomSqlSegment());
// 模拟JSON数据路径
String path = "classpath:org/jeecg/modules/dlglong/json/ddjh.json";
String statusValue = "8";
if (statusValue.equals(status)) {
path = "classpath:org/jeecg/modules/dlglong/json/ddjh_s8.json";
}
// 读取JSON数据
JSONArray dataList = readJsonData(path);
if (dataList == null) {
return Result.error("读取数据失败!");
}
IPage<JSONObject> page = this.queryDataPage(dataList, parentId, pageNo, pageSize);
// 逐行查询子表数据,用于计算拖轮状态
List<JSONObject> records = page.getRecords();
for (JSONObject record : records) {
Map<String, Integer> tugStatusMap = new HashMap<>(5);
String id = record.getString("id");
// 查询出主表的拖轮
String tugMain = record.getString("tug");
// 判断是否有值
if (StringUtils.isNotBlank(tugMain)) {
// 拖轮根据分号分割
String[] tugs = tugMain.split(";");
// 查询子表数据
List<JSONObject> subRecords = this.queryDataPage(dataList, id, null, null).getRecords();
// 遍历子表和拖轮数据,找出进行计算反推拖轮状态
for (JSONObject subData : subRecords) {
String subTug = subData.getString("tug");
if (StringUtils.isNotBlank(subTug)) {
for (String tug : tugs) {
if (tug.equals(subTug)) {
// 计算拖轮状态逻辑
int statusCode = 0;
/* 如果有发船时间、作业开始时间、作业结束时间、回船时间,则主表中的拖轮列中的每个拖轮背景色要即时变色 */
// 有发船时间,状态 +1
String departureTime = subData.getString("departure_time");
if (StringUtils.isNotBlank(departureTime)) {
statusCode += 1;
}
// 有作业开始时间,状态 +1
String workBeginTime = subData.getString("work_begin_time");
if (StringUtils.isNotBlank(workBeginTime)) {
statusCode += 1;
}
// 有作业结束时间,状态 +1
String workEndTime = subData.getString("work_end_time");
if (StringUtils.isNotBlank(workEndTime)) {
statusCode += 1;
}
// 有回船时间,状态 +1
String returnTime = subData.getString("return_time");
if (StringUtils.isNotBlank(returnTime)) {
statusCode += 1;
}
// 保存拖轮状态key是拖轮的值value是状态前端根据不同的状态码显示不同的颜色这个颜色也可以后台计算完之后返回给前端直接使用
tugStatusMap.put(tug, statusCode);
break;
}
}
}
}
}
// 新加一个字段用于保存拖轮状态,不要直接覆盖原来的,这个字段可以不保存到数据库里
record.put("tug_status", tugStatusMap);
}
page.setRecords(records);
return Result.ok(page);
}
/**
* 模拟查询数据可以根据父ID查询可以分页
*
* @param dataList 数据列表
* @param parentId 父ID
* @param pageNo 页码
* @param pageSize 页大小
* @return
*/
private IPage<JSONObject> queryDataPage(JSONArray dataList, String parentId, Integer pageNo, Integer pageSize) {
// 根据父级id查询子级
JSONArray dataDb = dataList;
if (StringUtils.isNotBlank(parentId)) {
JSONArray results = new JSONArray();
List<String> parentIds = Arrays.asList(parentId.split(","));
this.queryByParentId(dataDb, parentIds, results);
dataDb = results;
}
// 模拟分页实际中应用SQL自带的分页
List<JSONObject> records = new ArrayList<>();
IPage<JSONObject> page;
long beginIndex, endIndex;
// 如果任意一个参数为null则不分页
if (pageNo == null || pageSize == null) {
page = new Page<>(0, dataDb.size());
beginIndex = 0;
endIndex = dataDb.size();
} else {
page = new Page<>(pageNo, pageSize);
beginIndex = page.offset();
endIndex = page.offset() + page.getSize();
}
for (long i = beginIndex; (i < endIndex && i < dataDb.size()); i++) {
JSONObject data = dataDb.getJSONObject((int) i);
data = JSON.parseObject(data.toJSONString());
// 不返回 children
data.remove("children");
records.add(data);
}
page.setRecords(records);
page.setTotal(dataDb.size());
return page;
}
private void queryByParentId(JSONArray dataList, List<String> parentIds, JSONArray results) {
for (int i = 0; i < dataList.size(); i++) {
JSONObject data = dataList.getJSONObject(i);
JSONArray children = data.getJSONArray("children");
// 找到了该父级
if (parentIds.contains(data.getString("id"))) {
if (children != null) {
// addAll 的目的是将多个子表的数据合并在一起
results.addAll(children);
}
} else {
if (children != null) {
queryByParentId(children, parentIds, results);
}
}
}
results.addAll(new JSONArray());
}
private JSONArray readJsonData(String path) {
try {
InputStream stream = getClass().getClassLoader().getResourceAsStream(path.replace("classpath:", ""));
if (stream != null) {
String json = IOUtils.toString(stream, "UTF-8");
return JSON.parseArray(json);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return null;
}
/**
* 获取车辆最后一个位置
*
* @return
*/
@PostMapping("/findLatestCarLngLat")
public List findLatestCarLngLat() {
// 模拟JSON数据路径
String path = "classpath:org/jeecg/modules/dlglong/json/CarLngLat.json";
// 读取JSON数据
return readJsonData(path);
}
/**
* 获取车辆最后一个位置
*
* @return
*/
@PostMapping("/findCarTrace")
public List findCarTrace() {
// 模拟JSON数据路径
String path = "classpath:org/jeecg/modules/dlglong/json/CarTrace.json";
// 读取JSON数据
return readJsonData(path);
}
}

View File

@ -1,27 +0,0 @@
package org.jeecg.modules.dlglong.entity;
import lombok.Data;
/**
* 模拟实体
* @author: jeecg-boot
*/
@Data
public class MockEntity {
/**
* id
*/
private String id;
/**
* 父级ID
*/
private String parentId;
/**
* 状态
*/
private String status;
/* -- 省略其他字段 -- */
}

View File

@ -1,14 +0,0 @@
[
{
"id": "6891ba44421aa907bcb7390c",
"alarm": "0",
"altitude": "13",
"direction": "0",
"latitude": "38.918739",
"longitude": "117.758737",
"speed": "11",
"status": "4980739",
"timestamp": "2025-08-05T16:01:07",
"imei": "18441136860"
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 B

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 B

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1015 B

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 B

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 B

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 B

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 B

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 B

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 B

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 B

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 B

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 B

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 B

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 B

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,370 +0,0 @@
{
"list": [
{
"key": "1717072932495_439966",
"type": "card",
"isAutoGrid": true,
"isContainer": true,
"list": [
{
"type": "input",
"name": "名称",
"className": "form-input",
"icon": "icon-input",
"hideTitle": false,
"options": {
"width": "100%",
"defaultValue": "",
"required": true,
"dataType": null,
"pattern": "",
"placeholder": "",
"clearable": false,
"readonly": false,
"disabled": false,
"fillRuleCode": "",
"showPassword": false,
"unique": false,
"hidden": false,
"hiddenOnAdd": false,
"fieldNote": "",
"autoWidth": 50
},
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "string",
"allowFunc": true,
"valueSplit": "",
"customConfig": false
}
},
"remoteAPI": {
"url": "",
"executed": false
},
"key": "1717072932495_556479",
"model": "input_1717072932495_556479",
"modelType": "main",
"rules": [
{
"required": true,
"message": "${title}必须填写"
}
],
"isSubItem": false
},
{
"type": "number",
"name": "数字",
"className": "form-number",
"icon": "icon-number",
"hideTitle": false,
"options": {
"width": "",
"required": false,
"defaultValue": 0,
"placeholder": "",
"controls": false,
"min": 0,
"minUnlimited": true,
"max": 100,
"maxUnlimited": true,
"step": 1,
"disabled": false,
"controlsPosition": "right",
"unitText": "",
"unitPosition": "suffix",
"showPercent": false,
"align": "left",
"hidden": false,
"hiddenOnAdd": false,
"fieldNote": "",
"autoWidth": 50
},
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "number",
"allowFunc": true,
"valueSplit": "",
"customConfig": false
}
},
"remoteAPI": {
"url": "",
"executed": false
},
"key": "1717072985868_606195",
"model": "number_1717072985868_606195",
"modelType": "main",
"rules": [],
"isSubItem": false
}
],
"options": {
"required": false,
"hiddenOnAdd": false,
"hidden": false,
"fieldNote": ""
},
"model": "card_1717072932495_439966",
"hideTitle": false,
"modelType": "main"
},
{
"key": "1717072988159_545097",
"type": "card",
"isAutoGrid": true,
"isContainer": true,
"list": [
{
"type": "money",
"name": "金额",
"className": "form-money",
"icon": "icon-money",
"hideTitle": false,
"options": {
"width": "180px",
"placeholder": "请输入金额",
"required": false,
"unitText": "元",
"unitPosition": "suffix",
"precision": 2,
"hidden": false,
"disabled": false,
"hiddenOnAdd": false,
"fieldNote": "",
"autoWidth": 50
},
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "number",
"allowFunc": true,
"valueSplit": "",
"customConfig": false
}
},
"remoteAPI": {
"url": "",
"executed": false
},
"key": "1717072988159_568693",
"model": "money_1717072988159_568693",
"modelType": "main",
"rules": [],
"isSubItem": false
},
{
"type": "select",
"name": "下拉选择框",
"className": "form-select",
"icon": "icon-select",
"hideTitle": false,
"options": {
"defaultValue": "",
"multiple": false,
"disabled": false,
"clearable": true,
"placeholder": "",
"required": false,
"showLabel": false,
"showType": "default",
"width": "",
"useColor": false,
"colorIteratorIndex": 3,
"options": [
{
"value": "下拉框1",
"itemColor": "#2196F3"
},
{
"value": "下拉框2",
"itemColor": "#08C9C9"
},
{
"value": "下拉框3",
"itemColor": "#00C345"
}
],
"remote": false,
"filterable": false,
"remoteOptions": [],
"props": {
"value": "value",
"label": "label"
},
"remoteFunc": "",
"hidden": false,
"hiddenOnAdd": false,
"fieldNote": "",
"autoWidth": 50
},
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "string",
"allowFunc": true,
"valueSplit": ",",
"customConfig": true
}
},
"remoteAPI": {
"url": "",
"executed": false
},
"key": "1717072991431_622198",
"model": "select_1717072991431_622198",
"modelType": "main",
"rules": [],
"isSubItem": false
}
],
"options": {
"required": false,
"hiddenOnAdd": false,
"hidden": false,
"fieldNote": ""
},
"model": "card_1717072988159_545097",
"hideTitle": false,
"modelType": "main"
},
{
"key": "1717072932495_382575",
"type": "card",
"isAutoGrid": true,
"isContainer": true,
"list": [
{
"type": "imgupload",
"name": "图片上传",
"className": "form-tupian",
"icon": "icon-tupian",
"hideTitle": false,
"options": {
"defaultValue": [],
"size": {
"width": 100,
"height": 100
},
"width": "",
"tokenFunc": "funcGetToken",
"token": "",
"domain": "http://img.h5huodong.com",
"disabled": false,
"length": 9,
"multiple": true,
"hidden": false,
"hiddenOnAdd": false,
"required": false,
"fieldNote": "",
"autoWidth": 50
},
"key": "1717072996509_795340",
"model": "imgupload_1717072996509_795340",
"modelType": "main",
"rules": [],
"isSubItem": false
},
{
"type": "file-upload",
"name": "附件",
"className": "form-file-upload",
"icon": "icon-shangchuan",
"hideTitle": false,
"options": {
"defaultValue": [],
"token": "",
"length": 1,
"drag": false,
"multiple": false,
"disabled": false,
"buttonText": "添加附件",
"tokenFunc": "funcGetToken",
"hidden": false,
"hiddenOnAdd": false,
"required": false,
"fieldNote": "",
"autoWidth": 50
},
"key": "1717072932495_669325",
"model": "file_upload_1717072932495_669325",
"modelType": "main",
"rules": [],
"isSubItem": false
}
],
"options": {
"required": false,
"hiddenOnAdd": false,
"hidden": false,
"fieldNote": ""
},
"model": "card_1717072932495_382575",
"hideTitle": false,
"modelType": "main"
}
],
"config": {
"titleField": "input_1717072932495_556479",
"showHeaderTitle": true,
"labelWidth": 100,
"labelPosition": "top",
"size": "small",
"dialogOptions": {
"top": 20,
"width": 1000,
"padding": {
"top": 25,
"right": 25,
"bottom": 30,
"left": 25
}
},
"disabledAutoGrid": false,
"designMobileView": false,
"enableComment": true,
"hasWidgets": [
"input",
"number",
"card",
"money",
"select",
"imgupload",
"file-upload"
],
"expand": {
"js": "",
"css": "",
"url": {
"js": "",
"css": ""
}
},
"transactional": true,
"customRequestURL": [
{
"url": ""
}
],
"allowExternalLink": false,
"externalLinkShowData": false,
"headerImgUrl": "",
"externalTitle": "",
"enableNotice": false,
"noticeMode": "external",
"noticeType": "system",
"noticeReceiver": "",
"allowPrint": false,
"allowJmReport": false,
"jmReportURL": "",
"bizRuleConfig": [],
"bigDataMode": false
}
}

View File

@ -1,496 +0,0 @@
{
"list": [
{
"hideTitle": false,
"options": {
"hidden": false,
"hiddenOnAdd": false,
"fieldNote": "",
"required": false
},
"isContainer": true,
"model": "card_1717072902303_783177",
"modelType": "main",
"type": "card",
"isAutoGrid": true,
"list": [
{
"isSubItem": false,
"remoteAPI": {
"executed": false,
"url": ""
},
"icon": "icon-input",
"className": "form-input",
"rules": [
{
"required": true,
"message": "${title}必须填写"
}
],
"modelType": "main",
"type": "input",
"hideTitle": false,
"name": "名称",
"options": {
"clearable": false,
"hidden": false,
"defaultValue": "",
"pattern": "",
"fillRuleCode": "",
"fieldNote": "",
"required": true,
"readonly": false,
"unique": false,
"hiddenOnAdd": false,
"width": "100%",
"autoWidth": 100,
"showPassword": false,
"disabled": false,
"placeholder": ""
},
"model": "input_1717072902303_477529",
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "string",
"allowFunc": true,
"valueSplit": "",
"customConfig": false
}
},
"key": "1717072902303_477529"
}
],
"key": "1717072902303_783177"
},
{
"options": {
"required": false,
"hiddenOnAdd": false,
"hidden": false,
"fieldNote": ""
},
"isContainer": true,
"model": "card_1717073019436_526262",
"type": "card",
"isAutoGrid": true,
"list": [
{
"isSubItem": false,
"remoteAPI": {
"executed": false,
"url": ""
},
"icon": "icon-number",
"className": "form-number",
"rules": [],
"modelType": "main",
"type": "number",
"hideTitle": false,
"name": "数字",
"options": {
"controls": false,
"showPercent": false,
"hidden": false,
"max": 100,
"defaultValue": 0,
"unitPosition": "suffix",
"fieldNote": "",
"maxUnlimited": true,
"align": "left",
"required": false,
"min": 0,
"minUnlimited": true,
"hiddenOnAdd": false,
"width": "",
"autoWidth": 50,
"step": 1,
"disabled": false,
"placeholder": "",
"controlsPosition": "right",
"unitText": ""
},
"model": "number_1717073019436_586474",
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "number",
"allowFunc": true,
"valueSplit": "",
"customConfig": false
}
},
"key": "1717073019436_586474"
},
{
"isSubItem": false,
"remoteAPI": {
"executed": false,
"url": ""
},
"icon": "icon-money",
"className": "form-money",
"rules": [],
"modelType": "main",
"type": "money",
"hideTitle": false,
"name": "金额",
"options": {
"hidden": false,
"precision": 2,
"hiddenOnAdd": false,
"width": "180px",
"autoWidth": 50,
"unitPosition": "suffix",
"disabled": false,
"fieldNote": "",
"placeholder": "请输入金额",
"required": false,
"unitText": "元"
},
"model": "money_1717073021100_526660",
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "number",
"allowFunc": true,
"valueSplit": "",
"customConfig": false
}
},
"key": "1717073021100_526660"
}
],
"key": "1717073019436_526262",
"hideTitle": false,
"modelType": "main"
},
{
"hideTitle": false,
"options": {
"hidden": false,
"hiddenOnAdd": false,
"fieldNote": "",
"required": false
},
"isContainer": true,
"model": "card_1717072902303_118977",
"modelType": "main",
"type": "card",
"isAutoGrid": true,
"list": [
{
"isSubItem": false,
"remoteAPI": {
"executed": false,
"url": ""
},
"icon": "icon-select",
"className": "form-select",
"rules": [],
"modelType": "main",
"type": "select",
"hideTitle": false,
"name": "下拉选择框",
"options": {
"remoteFunc": "",
"filterable": false,
"clearable": true,
"hidden": false,
"defaultValue": "",
"remoteOptions": [],
"multiple": false,
"fieldNote": "",
"remote": false,
"required": false,
"showLabel": false,
"useColor": false,
"props": {
"label": "label",
"value": "value"
},
"colorIteratorIndex": 3,
"hiddenOnAdd": false,
"width": "",
"options": [
{
"itemColor": "#2196F3",
"value": "下拉框1"
},
{
"itemColor": "#08C9C9",
"value": "下拉框2"
},
{
"itemColor": "#00C345",
"value": "下拉框3"
}
],
"autoWidth": 50,
"showType": "default",
"disabled": false,
"placeholder": ""
},
"model": "select_1717073033259_273399",
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "string",
"allowFunc": true,
"valueSplit": ",",
"customConfig": true
}
},
"key": "1717073033259_273399"
},
{
"isSubItem": false,
"remoteAPI": {
"executed": false,
"url": ""
},
"icon": "icon-textarea",
"className": "form-textarea",
"rules": [],
"modelType": "main",
"type": "textarea",
"hideTitle": false,
"name": "描述",
"options": {
"readonly": false,
"hidden": false,
"defaultValue": "",
"unique": false,
"hiddenOnAdd": false,
"width": "100%",
"pattern": "",
"autoWidth": 50,
"disabled": false,
"fieldNote": "",
"placeholder": "",
"required": false
},
"model": "textarea_1717072902303_129466",
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "string",
"allowFunc": true,
"valueSplit": "",
"customConfig": false
}
},
"key": "1717072902303_129466"
}
],
"key": "1717072902303_118977"
},
{
"hideTitle": false,
"options": {
"hidden": false,
"hiddenOnAdd": false,
"fieldNote": "",
"required": false
},
"isContainer": true,
"model": "card_1717072902304_736053",
"modelType": "main",
"type": "card",
"isAutoGrid": true,
"list": [
{
"hideTitle": false,
"isSubItem": false,
"name": "图片上传",
"icon": "icon-tupian",
"options": {
"hidden": false,
"defaultValue": [],
"length": 9,
"multiple": true,
"fieldNote": "",
"required": false,
"token": "",
"size": {
"width": 100,
"height": 100
},
"tokenFunc": "funcGetToken",
"domain": "http://img.h5huodong.com",
"hiddenOnAdd": false,
"width": "",
"autoWidth": 50,
"disabled": false
},
"className": "form-tupian",
"model": "imgupload_1717073025137_563739",
"rules": [],
"modelType": "main",
"type": "imgupload",
"key": "1717073025137_563739"
},
{
"hideTitle": false,
"isSubItem": false,
"name": "附件",
"icon": "icon-shangchuan",
"options": {
"buttonText": "添加附件",
"hidden": false,
"defaultValue": [],
"length": 1,
"multiple": false,
"fieldNote": "",
"required": false,
"token": "",
"tokenFunc": "funcGetToken",
"hiddenOnAdd": false,
"autoWidth": 50,
"disabled": false,
"drag": false
},
"className": "form-file-upload",
"model": "file_upload_1717072902304_442777",
"rules": [],
"modelType": "main",
"type": "file-upload",
"key": "1717072902304_442777"
}
],
"key": "1717072902304_736053"
},
{
"isSubItem": false,
"remoteAPI": {
"url": "",
"executed": false
},
"icon": "icon-link",
"className": "form-link-record",
"rules": [],
"modelType": "main",
"type": "link-record",
"hideTitle": false,
"name": "主表@表单控件",
"options": {
"sourceCode": "ai_control_main",
"showMode": "single",
"showType": "card",
"titleField": "wen_ben",
"showFields": [],
"allowView": true,
"allowEdit": true,
"allowAdd": true,
"allowSelect": true,
"buttonText": "添加记录",
"twoWayModel": "sub_table_design_1717137038626_791984",
"dataSelectAuth": "all",
"filters": [
{
"matchType": "AND",
"rules": []
}
],
"search": {
"enabled": false,
"field": "",
"rule": "like",
"afterShow": false,
"fields": []
},
"createMode": {
"add": true,
"select": false,
"params": {
"selectLinkModel": ""
}
},
"width": "100%",
"defaultValue": "",
"defaultValType": "none",
"required": false,
"disabled": false,
"hidden": false,
"hiddenOnAdd": false,
"fieldNote": ""
},
"model": "link_record_1717137044235_306956",
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "string",
"allowFunc": true,
"valueSplit": "",
"customConfig": true
}
},
"key": "1717137044235_306956"
}
],
"config": {
"jmReportURL": "",
"enableComment": true,
"dialogOptions": {
"padding": {
"top": 25,
"left": 25,
"bottom": 30,
"right": 25
},
"top": 20,
"width": 1000
},
"allowJmReport": false,
"labelWidth": 100,
"headerImgUrl": "",
"noticeMode": "external",
"noticeReceiver": "",
"designMobileView": false,
"labelPosition": "top",
"allowPrint": false,
"enableNotice": false,
"bizRuleConfig": [],
"showHeaderTitle": true,
"bigDataMode": false,
"titleField": "input_1717072902303_477529",
"externalTitle": "",
"noticeType": "system",
"customRequestURL": [
{
"url": ""
}
],
"hasWidgets": [
"input",
"card",
"number",
"money",
"select",
"textarea",
"imgupload",
"file-upload",
"link-record"
],
"expand": {
"css": "",
"js": "",
"url": {
"css": "",
"js": ""
}
},
"size": "small",
"disabledAutoGrid": false,
"allowExternalLink": false,
"externalLinkShowData": false,
"transactional": true
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>jeecg-boot-parent</artifactId>
<groupId>org.jeecgframework.boot3</groupId>
<version>3.8.3</version>
<version>3.9.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>