From 0103c1c901ed244a2b7c660279609915575c99d9 Mon Sep 17 00:00:00 2001 From: xs Date: Wed, 14 Jan 2026 17:25:35 +0800 Subject: [PATCH] =?UTF-8?q?1.7.0=E5=90=8E=E7=AB=AF=20feat(AI):=E5=AE=8C?= =?UTF-8?q?=E6=88=90AI=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E5=8A=9F=E8=83=BD=EF=BC=9B=20fix(AI)=EF=BC=9A?= =?UTF-8?q?=E5=AE=8C=E5=96=84AI=E9=97=AE=E7=AD=94=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/constant/HwMomAiConstants.java | 7 + ruoyi-modules/hwmom-ai/pom.xml | 94 +- .../ai/asr/service/AliAsrServiceImpl.java | 116 +- .../ai/controller/AiAssistantController.java | 8 +- .../ai/controller/AiReportController.java | 160 ++ .../ai/domain/dto/AIReportRequest.java | 34 + .../ai/domain/dto/AIReportResponse.java | 63 + .../dromara/ai/domain/dto/AIReportResult.java | 21 + .../dromara/ai/domain/dto/StreamResult.java | 17 + .../mapper/SQLServerDatabaseMetaMapper.java | 7 + .../org/dromara/ai/process/dto/AIRequest.java | 6 + .../IUnifiedAIProviderProcessor.java | 4 + .../impl/BaseAIProviderProcessor.java | 48 +- .../processor/impl/DeepSeekProcessor.java | 73 +- .../processor/impl/TencentLkeProcessor.java | 6 + .../impl/TongYiQianWenProcessor.java | 6 + .../ai/service/IAIAssistantService.java | 2 + .../dromara/ai/service/IAIReportService.java | 74 + .../ai/service/IAiChatMessageService.java | 8 + .../service/impl/AIAssistantServiceImpl.java | 879 +++++++- .../ai/service/impl/AIReportServiceImpl.java | 1964 +++++++++++++++++ .../impl/AiChatMessageServiceImpl.java | 15 + .../mapper/ai/SQLServerDatabaseMetaMapper.xml | 5 + 23 files changed, 3390 insertions(+), 227 deletions(-) create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiReportController.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportRequest.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportResponse.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportResult.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/StreamResult.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAIReportService.java create mode 100644 ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIReportServiceImpl.java diff --git a/ruoyi-common/hwmom-common-mom/src/main/java/org/dromara/common/constant/HwMomAiConstants.java b/ruoyi-common/hwmom-common-mom/src/main/java/org/dromara/common/constant/HwMomAiConstants.java index 56a5be0f..ae9bc50b 100644 --- a/ruoyi-common/hwmom-common-mom/src/main/java/org/dromara/common/constant/HwMomAiConstants.java +++ b/ruoyi-common/hwmom-common-mom/src/main/java/org/dromara/common/constant/HwMomAiConstants.java @@ -17,15 +17,22 @@ public interface HwMomAiConstants { public static final String AI_FORM_SETTING_FIELD_TYPE_DICT = "3";//字典数据 + /** + * ai_chat_message的messagge_type和ai_chat_message_detail的message_detail_type + */ public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION = "1";//AI问答 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_KNOWLEDGE_QUESTION = "2";//AI知识库问答 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_SQL = "3";//AI生成SQL public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_FORM = "4";//AI填报 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION_VECTOR = "5";//提问获取向量 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_CONTENT_VECTOR = "6";//上传内容获取向量 + public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT_SQL = "7";//AI生成报表SQL + public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT = "8";//AI生成报表SQL public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_TEST = "9";//AI测试 + public static final String AI_ASR_RESULT_SUCCESS = "1";//语音识别结果:成功 + public static final String AI_ASR_RESULT_FAILED = "0";//语音识别结果:失败 } diff --git a/ruoyi-modules/hwmom-ai/pom.xml b/ruoyi-modules/hwmom-ai/pom.xml index 18f90f0b..1b0e9f18 100644 --- a/ruoyi-modules/hwmom-ai/pom.xml +++ b/ruoyi-modules/hwmom-ai/pom.xml @@ -53,6 +53,17 @@ ruoyi-common-mybatis + + org.apache.commons + commons-compress + 1.25.0 + + + + org.dromara + ruoyi-common-excel + + org.dromara ruoyi-common-dubbo @@ -114,18 +125,7 @@ - - org.dromara - hwmom-api-mes - 2.2.2 - compile - - - org.dromara - hwmom-api-pda - 2.2.2 - compile - + @@ -175,16 +175,16 @@ - - com.theokanning.openai-gpt3-java - service - 0.18.0 - - - com.theokanning.openai-gpt3-java - client - 0.18.0 - + + + + + + + + + + @@ -196,22 +196,22 @@ - - org.tensorflow - tensorflow-core-platform - 0.5.0 - - - org.deeplearning4j - deeplearning4j-core - 1.0.0-M2.1 - + + + + + + + + + + - - ai.djl - api - 0.28.0 - + + + + + ai.djl model-zoo @@ -294,16 +294,16 @@ - - org.apache.poi - poi-ooxml - 5.2.3 - - - commons-io - commons-io - 2.13.0 - + + + + + + + + + + org.anyline diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/asr/service/AliAsrServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/asr/service/AliAsrServiceImpl.java index 57d28790..d59220b6 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/asr/service/AliAsrServiceImpl.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/asr/service/AliAsrServiceImpl.java @@ -90,21 +90,27 @@ public class AliAsrServiceImpl implements AliAsrService { public String recognizeSpeechByUrl(String audioUrl) { try { // 构建识别参数 - if(StringUtils.isEmpty(apiKey) || StringUtils.isEmpty(modelName)){ + if (StringUtils.isEmpty(apiKey) || StringUtils.isEmpty(modelName)) { throw new RuntimeException("请配置apiKey和modelName"); } TranscriptionParam param = TranscriptionParam.builder() .apiKey(EncryptUtils.decryptByBase64(apiKey)) .model(modelName) +// .model("paraformer-v2") .fileUrls(Arrays.asList(audioUrl)) .parameter("language_hints", LANGUAGE_HINTS) .build(); Transcription transcription = new Transcription(); + // 提交转写请求 + TranscriptionResult result = transcription.asyncCall(param); + System.out.println("RequestId: " + result.getRequestId()); + // 阻塞等待任务完成并获取结果 + result = transcription.wait( + TranscriptionQueryParam.FromTranscriptionParam(param, result.getTaskId())); // 提交语音识别任务 - TranscriptionResult result = transcription.asyncCall(param); String taskId = result.getTaskId(); logger.info("语音识别任务提交成功,TaskId: {}", taskId); @@ -118,24 +124,24 @@ public class AliAsrServiceImpl implements AliAsrService { aiAsrRecord.setCreateDept(LoginHelper.getDeptId()); aiAsrRecord.setCreateTime(new Date()); - while (true) { - result = transcription.fetch(TranscriptionQueryParam.FromTranscriptionParam(param, taskId)); - - if (result.getTaskStatus() == TaskStatus.SUCCEEDED) { - logger.info("语音识别任务完成,TaskId: {}", taskId); - break; - } else if (result.getTaskStatus() == TaskStatus.FAILED) { - aiAsrRecord.setAsrFailedReason(result.getResults().toString()); - aiAsrRecord.setTaskId(taskId); - aiAsrRecord.setAsrResult(HwMomAiConstants.AI_ASR_RESULT_FAILED); - aiAsrRecordMapper.insert(aiAsrRecord); - logger.error("语音识别任务失败,TaskId: {}, 错误信息: {}", taskId, result.getResults().toString()); - throw new RuntimeException("语音识别任务失败: " + result.getResults().toString()); - } - - // 等待1秒后再次查询 - Thread.sleep(1000); - } +// while (true) { +// result = transcription.fetch(TranscriptionQueryParam.FromTranscriptionParam(param, taskId)); +// +// if (result.getTaskStatus() == TaskStatus.SUCCEEDED) { +// logger.info("语音识别任务完成,TaskId: {}", taskId); +// break; +// } else if (result.getTaskStatus() == TaskStatus.FAILED) { +// aiAsrRecord.setAsrFailedReason(result.getResults().toString()); +// aiAsrRecord.setTaskId(taskId); +// aiAsrRecord.setAsrResult(HwMomAiConstants.AI_ASR_RESULT_FAILED); +// aiAsrRecordMapper.insert(aiAsrRecord); +// logger.error("语音识别任务失败,TaskId: {}, 错误信息: {}", taskId, result.getResults().toString()); +// throw new RuntimeException("语音识别任务失败: " + result.getResults().toString()); +// } +// +// // 等待1秒后再次查询 +// Thread.sleep(1000); +// } // 解析识别结果 String recognitionResult = parseRecognitionResult(result); @@ -161,20 +167,23 @@ public class AliAsrServiceImpl implements AliAsrService { throw new RuntimeException("未获取到识别结果"); } - TranscriptionTaskResult taskResult = taskResultList.get(0); - String transcriptionUrl = taskResult.getTranscriptionUrl(); - // 获取识别结果详情 - HttpURLConnection connection = (HttpURLConnection) new URL(transcriptionUrl).openConnection(); - connection.setRequestMethod("GET"); - connection.connect(); - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - JsonElement jsonElement = gson.fromJson(reader, JsonElement.class); - - if (jsonElement.isJsonObject()) { - JsonObject jsonObject = jsonElement.getAsJsonObject(); + if (taskResultList != null && taskResultList.size() > 0) { + HttpURLConnection connection = null; + try { + TranscriptionTaskResult taskResult = taskResultList.get(0); + String transcriptionUrl = taskResult.getTranscriptionUrl(); + connection = + (HttpURLConnection) new URL(transcriptionUrl).openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + BufferedReader reader = + new BufferedReader(new InputStreamReader(connection.getInputStream())); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + JsonElement jsonResult = gson.fromJson(reader, JsonObject.class); + System.out.println(gson.toJson(jsonResult)); + if (jsonResult.isJsonObject()) { + JsonObject jsonObject = jsonResult.getAsJsonObject(); JsonArray transcriptsArr = jsonObject.getAsJsonArray("transcripts"); if (transcriptsArr != null && !transcriptsArr.isEmpty()) { @@ -183,11 +192,44 @@ public class AliAsrServiceImpl implements AliAsrService { return extractTextFromTags(text); } } - - throw new RuntimeException("解析识别结果失败"); - } finally { - connection.disconnect(); + } catch (Exception e) { + throw new RuntimeException("语音识别失败:" + e.getMessage()); + } finally { + if (connection != null) { + connection.disconnect(); + } + } } + + +// TranscriptionTaskResult taskResult = taskResultList.get(0); +// String transcriptionUrl = taskResult.getTranscriptionUrl(); +// +// // 获取识别结果详情 +// HttpURLConnection connection = (HttpURLConnection) new URL(transcriptionUrl).openConnection(); +// connection.setRequestMethod("GET"); +// connection.connect(); +// +// try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { +// Gson gson = new GsonBuilder().setPrettyPrinting().create(); +// JsonElement jsonElement = gson.fromJson(reader, JsonElement.class); +// +// if (jsonElement.isJsonObject()) { +// JsonObject jsonObject = jsonElement.getAsJsonObject(); +// JsonArray transcriptsArr = jsonObject.getAsJsonArray("transcripts"); +// +// if (transcriptsArr != null && !transcriptsArr.isEmpty()) { +// JsonObject jsonObject1 = transcriptsArr.get(0).getAsJsonObject(); +// String text = jsonObject1.get("text").getAsString(); +// return extractTextFromTags(text); +// } +// } +// +// throw new RuntimeException("解析识别结果失败"); +// } finally { +// connection.disconnect(); +// } + return null; } /** diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiAssistantController.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiAssistantController.java index 313dc926..e77b9f5d 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiAssistantController.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiAssistantController.java @@ -25,6 +25,7 @@ import org.dromara.ai.process.provider.processor.AIProviderProcessorFactory; import org.dromara.ai.process.provider.processor.IUnifiedAIProviderProcessor; import org.dromara.ai.service.*; import org.dromara.ai.vectordb.service.IVectorDBService; +import org.dromara.common.constant.HwMomAiConstants; import org.dromara.common.core.domain.R; import org.dromara.common.core.validate.EditGroup; import org.dromara.common.idempotent.annotation.RepeatSubmit; @@ -68,11 +69,11 @@ public class AiAssistantController extends BaseController { } /** - * 下拉框查询AI模型列表 + * 查询AI模型列表(AI问答左侧列表) */ @GetMapping("/getAiChatMessageList") public R> getAiChatMessageList(AiChatMessageBo bo) { - List list = aiChatMessageService.queryList(bo); + List list = aiChatMessageService.queryQaList(bo); return R.ok(list); } @@ -125,6 +126,7 @@ public class AiAssistantController extends BaseController { * 非流式聊天接口 */ @PostMapping("/chat") + @SaCheckPermission("ai:aiChatMessage:list") public Mono chat( @RequestParam("provider") String provider, @RequestBody AIRequest request) { @@ -139,8 +141,10 @@ public class AiAssistantController extends BaseController { * @param request * @return */ + @SaCheckPermission("ai:aiAssistant:generateSql") @PostMapping(value = "/generateSql") public String generateSql(AIRequest request) { + request.setMessageDetailType(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_SQL); return aiAssistantService.generateSQL(request); } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiReportController.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiReportController.java new file mode 100644 index 00000000..4ca03985 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/controller/AiReportController.java @@ -0,0 +1,160 @@ +package org.dromara.ai.controller; + + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.alibaba.fastjson.JSONObject; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.dromara.ai.domain.AiChatMessage; +import org.dromara.ai.domain.bo.AiAsrRecordBo; +import org.dromara.ai.domain.bo.AiChatMessageDetailBo; +import org.dromara.ai.domain.bo.AiTokenUsageBo; +import org.dromara.ai.domain.dto.AIReportRequest; +import org.dromara.ai.domain.dto.AIReportResponse; +import org.dromara.ai.domain.vo.AiAsrRecordVo; +import org.dromara.ai.domain.vo.AiChatMessageDetailVo; +import org.dromara.ai.domain.vo.AiChatMessageVo; +import org.dromara.ai.domain.vo.AiTokenUsageVo; +import org.dromara.ai.process.dto.AIRequest; +import org.dromara.ai.service.IAIAssistantService; +import org.dromara.ai.service.IAIReportService; +import org.dromara.common.constant.HwMomAiConstants; +import org.dromara.common.core.domain.R; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.web.core.BaseController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * AI模型 + * 前端访问路由地址为:/ai/aiReport + * + * @author xins + * @date 2025-08-07 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/aiReport") +public class AiReportController extends BaseController { + private final Logger logger = LoggerFactory.getLogger(AiReportController.class); + + private final IAIReportService aiReportService; + + private final IAIAssistantService aiAssistantService; + + /** + * 根据生成报表页面用户输入生成sql,传入参数包括用户输入、AIModelID, AIPLATFORMID + * + * @param request + * @return + */ + @PostMapping(value = "/generateReportSql") + @SaCheckPermission("ai:aiReport:generate") + public R generateReportSql(AIRequest request) { + request.setMessageDetailType(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT_SQL); + return R.ok(aiAssistantService.generateSQL(request)); + } + + + @PostMapping("/generateReport") + @SaCheckPermission("ai:aiReport:generate") + @ResponseBody + public R generateReport(@RequestBody AIRequest request) { + String provider = "deepseek"; // 或从请求中获取 + request.setMessageDetailType(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT); + Mono aiReportResponseMono = aiReportService.generateReport(provider, request); +// System.out.println(aiReportResponseMono.block().getChartHtml()); + return R.ok(aiReportResponseMono.block()); + + } + + + + /** + * 获取最新AI生成报表信息 + */ + @GetMapping("/getRecentReports") + @SaCheckPermission("ai:aiReport:generate") + @ResponseBody + public TableDataInfo getRecentReports() { + return aiReportService.getRecentReports(); + } + + + /** + * 获取最新一条AI生成报表信息 + */ + @GetMapping("/getRecentReportInfo") + @SaCheckPermission("ai:aiReport:generate") + public R getRecentReportInfo(AiChatMessageDetailBo bo) { + AiChatMessageDetailVo aiChatMessageDetailVo = aiReportService.getRecentReportInfo(bo); + return R.ok(aiChatMessageDetailVo); + } + + + /** + * 导出用户token使用详情列表 + */ +// @SaCheckPermission("ai:tokenUsage:export") + @Log(title = "AI生成报表", businessType = BusinessType.EXPORT) + @PostMapping("/exportReportData") + @SaCheckPermission("ai:aiReport:export") + public void exportReportData(AIRequest aiRequest, HttpServletResponse response) { + aiReportService.exportReportData(aiRequest,response); + } + + + + + + @PostMapping(value = "/streamGenerate", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @SaCheckPermission("ai:aiReport:generate") + public Flux streamGenerateReport( + @RequestParam("provider") String provider, @RequestBody AIRequest request) { +// LoginUser loginUser = LoginHelper.getLoginUser(); +// IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory.getProcessor(provider); + return aiReportService.streamGenerateReport(provider, request); + } + + + /** + * 生成AI报表分析 + */ + @PostMapping("/generateTestReport") + @SaCheckPermission("ai:aiReport:generate") + @ResponseBody + public R generateTestReport(@RequestBody AIReportRequest request) { + try { + logger.info("收到AI报表生成请求: {}", request.getPrompt()); + + // 参数验证 + if (request.getPrompt() == null || request.getPrompt().trim().isEmpty()) { + return R.fail("请输入分析需求描述"); + } + + // 生成报表 + AIReportResponse response = aiReportService.generateTestReport(request); + + logger.info("AI报表生成成功,SQL: {}", response.getSql()); + return R.ok(response); + + } catch (Exception e) { + logger.error("AI报表生成失败", e); + return R.fail("报表生成失败: " + e.getMessage()); + } + } + +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportRequest.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportRequest.java new file mode 100644 index 00000000..9fc18c96 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportRequest.java @@ -0,0 +1,34 @@ +package org.dromara.ai.domain.dto; + +/** + * @Author xins + * @Date 2026/1/5 14:38 + * @Description:AI报表分析请求参数 + */ + +import lombok.Data; +import java.util.Map; + +@Data +public class AIReportRequest { + + /** + * 用户提问内容 + */ + private String prompt; + + /** + * 数据源ID + */ + private String dataSourceId; + + /** + * 图表类型偏好(可选) + */ + private String chartType; + + /** + * 其他参数 + */ + private Map parameters; +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportResponse.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportResponse.java new file mode 100644 index 00000000..0cc8ed28 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportResponse.java @@ -0,0 +1,63 @@ +package org.dromara.ai.domain.dto; + +/** + * @Author xins + * @Date 2026/1/5 14:39 + * @Description:AI报表分析响应结果 + */ + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class AIReportResponse { + + /** + * 生成的SQL语句 + */ + private String sql; + + /** + * 查询结果数据 + */ + private List> data; + + /** + * 数据字段信息 + */ + private List fields; + + /** + * 图表HTML字符串 + */ + private String chartHtml; + + /** + * 列表HTML字符串(支持复合表头) + */ + private String tableHtml; + + /** + * 分析建议 + */ + private String analysis; + + /** + * 执行时间(毫秒) + */ + private Long executionTime; + + /** + * 数据字段信息 + */ + @Data + public static class FieldInfo { + private String name; + private String type; + private String comment; + private boolean isPrimary; + private boolean isNumeric; + } +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportResult.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportResult.java new file mode 100644 index 00000000..2eb88948 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/AIReportResult.java @@ -0,0 +1,21 @@ +package org.dromara.ai.domain.dto; + +/** + * @Author xins + * @Date 2026/1/5 14:39 + * @Description:AI报表分析响应结果 + */ + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class AIReportResult { + + private Boolean success; + private String code; + private String message; + private AIReportResponse aiReportResponse; +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/StreamResult.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/StreamResult.java new file mode 100644 index 00000000..2329fe62 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/domain/dto/StreamResult.java @@ -0,0 +1,17 @@ +package org.dromara.ai.domain.dto; + +import lombok.Data; +import org.dromara.ai.process.dto.TokenUsage; + +/** + * @Author xins + * @Date 2026/1/7 10:17 + * @Description: + */ +@Data +public class StreamResult { + private String type; // "chunk" 或 "complete" + private String content; + private TokenUsage tokenUsage; + private Boolean isComplete = false; +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/SQLServerDatabaseMetaMapper.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/SQLServerDatabaseMetaMapper.java index f3c197f6..d583adec 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/SQLServerDatabaseMetaMapper.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/mapper/SQLServerDatabaseMetaMapper.java @@ -64,4 +64,11 @@ public interface SQLServerDatabaseMetaMapper { @Param("orderBy") String orderBy, @Param("groupBy") String groupBy); + + /** + * 动态SELECT查询 + * @param sql + */ + List> dynamicSelectSql(@Param("sql") String sql); + } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIRequest.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIRequest.java index 456c6918..b5c7131d 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIRequest.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/dto/AIRequest.java @@ -125,4 +125,10 @@ public class AIRequest { * 其他自定义参数 */ private Object customParams; + + private String messageDetailType; + + private String sql; + + private Long chatMessageId; } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java index 17f9a461..abd640d6 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/IUnifiedAIProviderProcessor.java @@ -2,6 +2,7 @@ package org.dromara.ai.process.provider.processor; import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; +import org.dromara.ai.domain.dto.StreamResult; import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIResponse; import org.dromara.ai.process.dto.TokenUsage; @@ -103,4 +104,7 @@ public interface IUnifiedAIProviderProcessor { Long modelId, Long knowledgeBaseId, Long knowledgeContentId, Long chatMessageId,String sessionId,String takeFlag,String completeFlag, Long userId,String tenantId,Long deptId); + + + public Flux chatStreamComplete(AIRequest request, LoginUser loginUser); } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java index e5951db9..cd348f8d 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/BaseAIProviderProcessor.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.yulichang.toolkit.JoinWrappers; import com.github.yulichang.wrapper.MPJLambdaWrapper; +import io.seata.common.util.StringUtils; import org.dromara.ai.domain.AiChatMessage; import org.dromara.ai.domain.AiChatMessageDetail; import org.dromara.ai.domain.AiTokenUsage; @@ -88,7 +89,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce } protected void configureApiKey(AIRequest request) { - if(request.getModelId()!=null){ + if (request.getModelId() != null) { AiModelVo aiModelVo = aiModelMapper.selectVoById(request.getModelId()); if (aiModelVo != null) { if (aiModelVo.getApiKey() == null) { @@ -101,12 +102,13 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce /** * 普通http请求(非流式请求),直接返回完整回复 + * * @param url * @param requestBody * @param apiKey * @return AIResponse */ - public abstract Mono standardRequest(String url,String requestBody, String apiKey); + public abstract Mono standardRequest(String url, String requestBody, String apiKey); /** @@ -165,14 +167,6 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce } - - - - - - - - /** * 处理流式响应的JSON块,返回包含内容和token信息的对象 */ @@ -216,6 +210,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce protected void saveChatMessage(AIRequest request, String fullResponse, TokenUsage tokenUsage, LoginUser loginUser) { try { + String messageType = StringUtils.isNotBlank(request.getMessageDetailType()) ? request.getMessageDetailType() : HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION; String sessionId = request.getSessionId(); AiChatMessage aiChatMessage = aiChatMessageMapper .selectOne(new LambdaQueryWrapper() @@ -225,7 +220,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce aiChatMessage = new AiChatMessage(); aiChatMessage.setSessionId(request.getSessionId()); aiChatMessage.setMessageTopic(objectMapper.writeValueAsString(request.getMessageTopic())); - aiChatMessage.setMessageType(AIChatMessageTypeEnum.AI_CHAT.getCode()); + aiChatMessage.setMessageType(messageType); aiChatMessage.setModelId(request.getModelId()); aiChatMessage.setKnowledgeBaseId(request.getKnowledgeBaseId()); // aiChatMessage.setTotalToken(); @@ -239,10 +234,10 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce } - saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION, objectMapper.writeValueAsString(request.getQuestionContent()), + saveTokenUsage(messageType, objectMapper.writeValueAsString(request.getQuestionContent()), objectMapper.writeValueAsString(fullResponse), tokenUsage, request.getModelId(), request.getKnowledgeBaseId(), null, - aiChatMessage.getChatMessageId(),request.getSessionId(),request.getCarryHistoryFlag(),"1", - loginUser.getUserId(),loginUser.getTenantId(), loginUser.getDeptId()); + aiChatMessage.getChatMessageId(), request.getSessionId(), request.getCarryHistoryFlag(), "1", + loginUser.getUserId(), loginUser.getTenantId(), loginUser.getDeptId()); // AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail(); @@ -275,6 +270,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce /** * 保存token使用情况 + * * @param messageDetailType * @param questionContent * @param answerContent @@ -292,12 +288,12 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce */ @Override public void saveTokenUsage(String messageDetailType, String questionContent, String answerContent, TokenUsage tokenUsage, - Long modelId, Long knowledgeBaseId, Long knowledgeContentId, - Long chatMessageId,String sessionId,String takeFlag,String completeFlag, - Long userId,String tenantId,Long deptId) { - Long promptToken = tokenUsage!=null ? tokenUsage.getPromptToken():null; - Long completionToken = tokenUsage!=null ? tokenUsage.getCompletionToken():null; - Long totalToken = tokenUsage!=null ? tokenUsage.getTotalToken():null; + Long modelId, Long knowledgeBaseId, Long knowledgeContentId, + Long chatMessageId, String sessionId, String takeFlag, String completeFlag, + Long userId, String tenantId, Long deptId) { + Long promptToken = tokenUsage != null ? tokenUsage.getPromptToken() : null; + Long completionToken = tokenUsage != null ? tokenUsage.getCompletionToken() : null; + Long totalToken = tokenUsage != null ? tokenUsage.getTotalToken() : null; AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail(); aiChatMessageDetail.setMessageDetailType(messageDetailType); @@ -350,7 +346,6 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce } - /** * 流式chunk结果类 */ @@ -364,8 +359,13 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce } // getters - public String getContent() { return content; } - public TokenUsage getTokenUsage() { return tokenUsage; } + public String getContent() { + return content; + } + + public TokenUsage getTokenUsage() { + return tokenUsage; + } public boolean hasContent() { return content != null && !content.isEmpty(); @@ -377,8 +377,6 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce } - - // /** // * 流式回复 // * @param request diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java index 40886afd..9a6a1e46 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/DeepSeekProcessor.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; import org.dromara.ai.domain.AiChatMessage; import org.dromara.ai.domain.AiChatMessageDetail; +import org.dromara.ai.domain.dto.StreamResult; import org.dromara.ai.domain.vo.AiModelVo; import org.dromara.ai.mapper.AiChatMessageDetailMapper; import org.dromara.ai.mapper.AiChatMessageMapper; @@ -53,16 +54,16 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { private final String deepSeekChatModel = "deepseek-chat"; - /** * 普通http请求(非流式请求),直接返回完整回复 + * * @param url * @param requestBody * @param apiKey * @return AIResponse */ @Override - public Mono standardRequest(String url,String requestBody, String apiKey) { + public Mono standardRequest(String url, String requestBody, String apiKey) { return webClient.post() .uri(url) .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) @@ -146,7 +147,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { }) .doOnComplete(() -> { // 流完成后保存到数据库 - saveChatMessage(request, fullResponseBuilder.toString(), null,loginUser); + saveChatMessage(request, fullResponseBuilder.toString(), null, loginUser); }) .doOnError(error -> { // 错误处理 @@ -308,7 +309,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { // usageNode.path("total_tokens").asInt() // ); - TokenUsage tokenUsage = new TokenUsage( usageNode.path("prompt_tokens").asLong(), + TokenUsage tokenUsage = new TokenUsage(usageNode.path("prompt_tokens").asLong(), usageNode.path("completion_tokens").asLong(), usageNode.path("total_tokens").asLong()); @@ -331,7 +332,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { } @Override - public List getEmbedding(GetEmbeddingResponse embeddingResponse){ + public List getEmbedding(GetEmbeddingResponse embeddingResponse) { return List.of(); } @@ -346,6 +347,63 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor { } + @Override + public Flux chatStreamComplete(AIRequest request, LoginUser loginUser) { + try { + ObjectNode rootNode = objectMapper.createObjectNode(); + rootNode.put("model", deepSeekChatModel); + rootNode.set("messages", objectMapper.valueToTree(request.getMessages())); + rootNode.put("stream", true); + + if (request.getTemperature() != null) { + rootNode.put("temperature", request.getTemperature()); + } + + String requestBody = objectMapper.writeValueAsString(rootNode); + configureApiKey(request); + + // 用于收集完整响应和token信息 + StringBuilder fullResponseBuilder = new StringBuilder(); + TokenUsage finalTokenUsage = new TokenUsage(0L, 0L, 0L); + + // 返回包装后的结果流 + return executeStreamRequest(API_URL, requestBody, request.getApiKey()) + .map(chunkResult -> { + // 收集内容 + if (chunkResult.hasContent()) { + fullResponseBuilder.append(chunkResult.getContent()); + } + + // 更新token使用信息 + if (chunkResult.hasTokenUsage()) { + TokenUsage usage = chunkResult.getTokenUsage(); + finalTokenUsage.setPromptToken(usage.getPromptToken()); + finalTokenUsage.setCompletionToken(usage.getCompletionToken()); + finalTokenUsage.setTotalToken(usage.getTotalToken()); + } + + // 创建流式结果 + StreamResult result = new StreamResult(); + result.setType("chunk"); + result.setContent(chunkResult.hasContent() ? chunkResult.getContent() : ""); + return result; + }) + .doOnComplete(() -> { + // 流完成后保存到数据库,但不发送额外内容 + System.out.println(fullResponseBuilder.toString()); + saveChatMessage(request, fullResponseBuilder.toString(), finalTokenUsage, loginUser); + }) + .doOnError(error -> { + // 即使出错也尝试保存已收集的内容 + saveChatMessage(request, fullResponseBuilder.toString(), finalTokenUsage, loginUser); + }); + } catch (IOException e) { + return Flux.error(new RuntimeException("构建请求失败: " + e.getMessage())); + } + } + + + private String extractContentRaw(String jsonStr) { // 简单的正则提取,作为最后手段 Pattern pattern = Pattern.compile("\"content\":\"(.*?)(? chatStreamComplete(AIRequest request, LoginUser loginUser) { + return null; + } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java index 72915803..cb190d9c 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/process/provider/processor/impl/TongYiQianWenProcessor.java @@ -12,6 +12,7 @@ import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; import org.checkerframework.checker.units.qual.A; import org.dromara.ai.domain.AiChatMessage; import org.dromara.ai.domain.AiChatMessageDetail; +import org.dromara.ai.domain.dto.StreamResult; import org.dromara.ai.mapper.AiChatMessageDetailMapper; import org.dromara.ai.mapper.AiChatMessageMapper; import org.dromara.ai.process.dto.AIMessage; @@ -431,4 +432,9 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor { protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception { return null; } + + @Override + public Flux chatStreamComplete(AIRequest request, LoginUser loginUser) { + return null; + } } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAIAssistantService.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAIAssistantService.java index 3b30c856..e171e0af 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAIAssistantService.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAIAssistantService.java @@ -1,6 +1,7 @@ package org.dromara.ai.service; import com.alibaba.fastjson.JSONObject; +import org.dromara.ai.domain.dto.AIReportResponse; import org.dromara.ai.process.dto.AIFillFormRequest; import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIResponse; @@ -33,4 +34,5 @@ public interface IAIAssistantService { */ public JSONObject aiFillForm(AIFillFormRequest aiFillFormRequest); // public boolean testAIModel(String provider, AIRequest aiRequest); + } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAIReportService.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAIReportService.java new file mode 100644 index 00000000..ff26dbe6 --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAIReportService.java @@ -0,0 +1,74 @@ +package org.dromara.ai.service; + + +import jakarta.servlet.http.HttpServletResponse; +import org.dromara.ai.domain.AiChatMessage; +import org.dromara.ai.domain.bo.AiChatMessageDetailBo; +import org.dromara.ai.domain.dto.AIReportRequest; +import org.dromara.ai.domain.dto.AIReportResponse; +import org.dromara.ai.domain.vo.AiChatMessageDetailVo; +import org.dromara.ai.domain.vo.AiChatMessageVo; +import org.dromara.ai.process.dto.AIRequest; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * AI报表分析服务接口 + */ +public interface IAIReportService { + + /** + * 流式聊天接口,待AI全部回复完再传给前端 + * + * @param provider + * @param aiRequest + * @return Flux + */ + public Mono generateReport(String provider, AIRequest aiRequest); + + /** + * 获取最近5条AI生成报表的信息 + * @return List + */ + public TableDataInfo getRecentReports(); + + + /** + * 根据sessionid和messagedetailtype获取一条最新的AI生成报表详细信息 + * @param aiChatMessageDetailBo + * @return AiChatMessageDetailVo + */ + public AiChatMessageDetailVo getRecentReportInfo(AiChatMessageDetailBo aiChatMessageDetailBo); + + + /** + * 导出AI生成报表的数据(excel格式) + * @param aiRequest + * @param response + */ + public void exportReportData(AIRequest aiRequest, HttpServletResponse response); + + + + + public Flux streamGenerateReport(String provider, AIRequest aiRequest); + + /** + * 分析并生成报表 + * @param request 分析请求 + * @return 报表响应结果 + */ + AIReportResponse generateTestReport(AIReportRequest request); + + /** + * 模拟AI生成SQL + * @param prompt 用户提问 + * @return 生成的SQL语句 + */ + String generateSQL(String prompt); + + +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageService.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageService.java index 8d5f1bec..1959f140 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageService.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/IAiChatMessageService.java @@ -88,4 +88,12 @@ public interface IAiChatMessageService { */ @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidBySessionId(String sessionId, Boolean isValid); + + /** + * 查询符合条件的聊天消息列表(不查询生成报表的数据) + * + * @param bo 查询条件 + * @return 聊天消息列表 + */ + public List queryQaList(AiChatMessageBo bo); } diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java index 9c7eec35..76fe091e 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIAssistantServiceImpl.java @@ -9,6 +9,7 @@ import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; import com.tencentcloudapi.lkeap.v20240522.models.Usage; import org.dromara.ai.domain.AiFormSettingDetail; import org.dromara.ai.domain.AiModel; +import org.dromara.ai.domain.dto.AIReportResponse; import org.dromara.ai.domain.dto.AiTableConditionWrapper; import org.dromara.ai.domain.dto.AiTableData; import org.dromara.ai.domain.dto.AiTableQueryCondition; @@ -22,6 +23,7 @@ import org.dromara.ai.service.IAIAssistantService; import org.dromara.ai.vectordb.service.IVectorDBService; import org.dromara.common.constant.HwMomAiConstants; import org.dromara.common.encrypt.utils.EncryptUtils; +import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.system.api.model.LoginUser; import org.springframework.beans.factory.annotation.Autowired; @@ -31,6 +33,7 @@ import reactor.core.publisher.Flux; import com.fasterxml.jackson.databind.ObjectMapper; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.*; import java.util.stream.Collectors; @@ -63,6 +66,417 @@ public class AIAssistantServiceImpl implements IAIAssistantService { @Autowired private SQLServerDatabaseMetaMapper sQLServerDatabaseMetaMapper; + private final String material_inventory_sql = "SELECT\n" + + "\tTOP 1000 wi.inventory_id,\n" + + "\twi.tenant_id,\n" + + "\twi.batch_code,\n" + + "\twi.material_id,\n" + + "\twi.location_code,\n" + + "\twi.material_categories,\n" + + "\twi.inventory_qty,\n" + + "\twi.update_time,\n" + + "\twi.lock_state,\n" + + "\twi.inventory_status,\n" + + "\twi.store_id,\n" + + "\twi.create_by,\n" + + "\twi.create_time,\n" + + "\twi.update_by,\n" + + "\twi.material_code,\n" + + "\twi.warehouse_id,\n" + + "\tbmi.material_name,\n" + + "\tbmi.material_spec,\n" + + "\tbmi.material_unit,\n" + + "\tbmt.matrial_type_name,\n" + + "\twpsp.store_place_name,\n" + + "\twpsp.store_place_code\n" + + "FROM\n" + + "\twms_inventory wi WITH(NOLOCK)\n" + + "LEFT JOIN base_material_info bmi WITH(NOLOCK) ON\n" + + "\tbmi.material_id = wi.material_id\n" + + "\tAND bmi.tenant_id = wi.tenant_id\n" + + "\tAND bmi.del_flag = '0'\n" + + "LEFT JOIN base_material_type bmt WITH(NOLOCK) ON\n" + + "\tbmt.matrial_type_id = bmi.material_type_id\n" + + "\tAND bmt.tenant_id = wi.tenant_id\n" + + "\tAND bmt.del_flag = '0'\n" + + "LEFT JOIN wms_psm_store_place wpsp WITH(NOLOCK) ON\n" + + "\twpsp.store_place_code = wi.location_code\n" + + "\tAND wpsp.tenant_id = wi.tenant_id\n" + + "WHERE\n" + + "\twi.inventory_status = '1'"; + + + private String reportHtml = "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + "
\n" + + "

\uD83D\uDCCA WMS库存分析报表

\n" + + " \n" + + "
\n" + + "
\n" + + "

物料种类数

\n" + + "
0
\n" + + "
\n" + + "
\n" + + "
\n" + + "

总库存量

\n" + + "
0.00
\n" + + "
单位
\n" + + "
\n" + + "
\n" + + "

平均库存量

\n" + + "
0.00
\n" + + "
单位/物料
\n" + + "
\n" + + "
\n" + + "

数据记录数

\n" + + "
0
\n" + + "
\n" + + "
\n" + + "
\n" + + "\n" + + "

物料库存分布

\n" + + "
\n" + + "\n" + + "

库存量TOP物料

\n" + + "
\n" + + "\n" + + "

物料分类统计

\n" + + "
\n" + + "\n" + + "

详细库存数据

\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
库存ID物料编码库位编码库存数量批次码库存状态锁定状态更新时间
\n" + + "\n" + + "
\n" + + " 报表生成时间: | 数据来源: WMS系统\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + ""; + + + /** * 流式聊天接口 * @@ -129,7 +543,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService { String messageContent = message.getContent().toString(); aiRequest.setText(messageContent); aiRequest.setTexts(new String[]{messageContent}); - StringBuilder sb = new StringBuilder(messageContent); +// StringBuilder sb = new StringBuilder(messageContent); Long embeddingModelId = aiRequest.getEmbeddingModelId(); AiModel aiModel = aiModelMapper.selectById(embeddingModelId); @@ -146,11 +560,11 @@ public class AIAssistantServiceImpl implements IAIAssistantService { GetEmbeddingResponse embeddingResponses = tencentLkeProcessor.getEmbeddingResponses(aiRequest); //获取token使用数量,然后保存 Usage usage = embeddingResponses.getUsage(); - TokenUsage tokenUsage = new TokenUsage(null,null,usage.getTotalTokens()); + TokenUsage tokenUsage = new TokenUsage(null, null, usage.getTotalTokens()); - tencentLkeProcessor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION_VECTOR,"AI问答获取向量", null, tokenUsage, - embeddingModelId, aiRequest.getKnowledgeBaseId(), null, - null, null, "0", "1",LoginHelper.getUserId(),LoginHelper.getTenantId(),LoginHelper.getDeptId()); + tencentLkeProcessor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION_VECTOR, "AI问答获取向量", null, tokenUsage, + embeddingModelId, aiRequest.getKnowledgeBaseId(), null, + null, null, "0", "1", LoginHelper.getUserId(), LoginHelper.getTenantId(), LoginHelper.getDeptId()); List queryEmbedding = tencentLkeProcessor.getEmbedding(embeddingResponses); @@ -165,16 +579,19 @@ public class AIAssistantServiceImpl implements IAIAssistantService { throw new RuntimeException("未找到相关知识库内容"); } - sb.append("\n####,请从以下知识库内容中获取答案:"); - for (String content : searchResultList) { - sb.append("\n####").append(content); - } +// sb.append("\n####,请从以下知识库内容中获取答案:"); +// for (String content : searchResultList) { +// sb.append("\n####").append(content); +// } // sb.append("\n####,LLM是人工智能大模型。agent是一个智能助手,agent的任务是回答用户的问题"); // sb.append(("\n\n注意:回答问题时,须严格根据我给你的系统上下文内容原文进行回答,请不要自己发挥,回答时保持原来文本的段落层级")); - sb.append((!searchResultList.isEmpty() ? "\n\n注意:回答问题时,须严格根据我给你的系统上下文内容原文进行回答,请不要自己发挥,回答时保持原来文本的段落层级" : "")); - message.setContent(sb.toString()); +// sb.append((!searchResultList.isEmpty() ? "\n\n注意:回答问题时,须严格根据我给你的系统上下文内容原文进行回答,请不要自己发挥,回答时保持原来文本的段落层级" : "")); + + String optimizedPrompt = buildKnowledgeBasedPrompt(messageContent, searchResultList); + + message.setContent(optimizedPrompt); return aiRequest; } catch (Exception e) { @@ -183,6 +600,49 @@ public class AIAssistantServiceImpl implements IAIAssistantService { } } + + + /** + * 构建知识库提示词 + */ + private String buildKnowledgeBasedPrompt(String userQuestion, List searchResults) { + StringBuilder promptBuilder = new StringBuilder(); + + if (searchResults.isEmpty()) { + // 没有找到相关知识库内容 + promptBuilder.append("用户问题:").append(userQuestion).append("\n\n"); + promptBuilder.append("【重要提示】当前知识库中没有找到相关内容。\n"); + promptBuilder.append("请直接回复:\"根据现有资料,无法回答此问题。\""); + return promptBuilder.toString(); + } + + // 构建结构化的prompt + promptBuilder.append("【指令说明】\n"); + promptBuilder.append("你是一个专业的信息检索助手,需要严格根据提供的知识库内容回答问题。\n\n"); + + promptBuilder.append("【知识库内容】\n"); + promptBuilder.append("以下是从知识库中检索到的相关信息:\n\n"); + + for (int i = 0; i < searchResults.size(); i++) { + promptBuilder.append("--- 知识片段 ").append(i + 1).append(" ---\n"); + promptBuilder.append(searchResults.get(i)).append("\n\n"); + } + + promptBuilder.append("【用户问题】\n"); + promptBuilder.append(userQuestion).append("\n\n"); + + promptBuilder.append("【回答要求】\n"); + promptBuilder.append("1. 必须严格基于上述知识库内容回答\n"); + promptBuilder.append("2. 如果知识库内容不足以回答问题,请明确说明\n"); + promptBuilder.append("3. 保持原文的准确性和细节,不要添加任何外部知识\n"); + promptBuilder.append("4. 如果知识库中有多个相关内容,请整合信息进行回答\n"); + promptBuilder.append("5. 使用清晰、有条理的方式组织答案\n\n"); + + promptBuilder.append("请基于知识库内容回答用户问题:"); + + return promptBuilder.toString(); + } + /** * 创建标准化的错误事件JSON字符串 */ @@ -220,6 +680,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService { @Override public String generateSQL(AIRequest aiRequest) { String naturalLanguageQuery = aiRequest.getText(); + // 1. 获取数据库结构 String dataName = StringUtils.isNotBlank(aiRequest.getDataName()) ? aiRequest.getDataName() : "master"; String schemaDescription = redisTemplate.opsForValue().get(HwMomAiConstants.AI_DATABASE_SCHEMA_KEY_PREFIX + dataName); @@ -228,72 +689,165 @@ public class AIAssistantServiceImpl implements IAIAssistantService { } JSONObject schemaJson = JSONObject.parseObject(schemaDescription); - StringBuilder sb = new StringBuilder("SQL Server 数据库结构:\n\n"); - schemaJson.entrySet().forEach(entry -> { -// sb.append(entry.getKey()).append("\n"); - sb.append(entry.getValue()).append("\n\n"); - }); -// sb.append(schemaDescription); + String formattedSchema = formatDatabaseSchema(schemaJson); - // 2. 构建 AI 提示 - String prompt = String.format( - "你是一个专业的 SQL Server 数据库专家。必须基于表结构生成SQL,其中的表名和字段名必须来自表结构信息(如果以下数据库结构中没有则返回select * from):\n\n%s\n\n" + - "请将以下自然语言查询转换为优化的 SQL Server T-SQL 语句:\n" + - "---\n%s\n---\n\n" + - "要求:\n" + - "1. 只返回 SQL 语句,不要包含解释\n" + - "2. 使用 SQL Server 特有的语法(如 TOP 而不是 LIMIT)\n" + - "3. 考虑性能优化\n" + - "4. 使用合适的索引提示(如果需要)\n" + - "5. 包含必要的 WITH(NOLOCK) 提示(适用于高并发环境)\n" + - "6. 使用 ANSI 标准的 JOIN 语法 \n", - sb.toString(), naturalLanguageQuery - ); + // 2. 构建AI提示词(使用优化后的模板) + String prompt = buildAIPrompt(formattedSchema, naturalLanguageQuery); + // 3. 调用AI服务 IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory.getProcessorByPlatformId(aiRequest.getPlatformId()); AIMessage aiMessage = new AIMessage(); aiMessage.setRole("user"); aiMessage.setContent(prompt); aiRequest.setMessages(Collections.singletonList(aiMessage)); + Mono response = processor.chat(aiRequest); - if (Objects.requireNonNull(response.block()).isSuccess()) { - String content = response.block().getContent().toString(); - //content内容需要转换,通过deepseek返回的如: -// ```sql -// SELECT -// dept_id, -// tenant_id, -// parent_id, -// ancestors, -// dept_name, -// dept_category, -// order_num, -// leader, -// phone, -// email, -// status, -// del_flag, -// create_dept, -// create_by, -// create_time, -// update_by, -// update_time -// FROM sys_dept WITH(NOLOCK) -// WHERE del_flag = '0' -// ORDER BY order_num, dept_id; -//``` - processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_SQL,prompt, content,response.block().getTokenUsage(), - aiRequest.getModelId(), null,null, - null,null,"0","1",LoginHelper.getUserId(),LoginHelper.getTenantId(),LoginHelper.getDeptId()); - return extractSqlFromContent(content); - - } else { - throw new RuntimeException("生成sql语句失败" + response.block().getErrorMessage()); - } + // 4. 处理响应 + return handleAIResponse(response, aiRequest, processor, prompt); } + /** + * 格式化数据库结构 + */ + private String formatDatabaseSchema(JSONObject schemaJson) { + StringBuilder sb = new StringBuilder("SQL Server 数据库结构:\n\n"); + + // 统计各类表,便于AI分析 + Map> tableCategories = new HashMap<>(); + + schemaJson.entrySet().forEach(entry -> { + String tableName = entry.getKey(); + String tableInfo = entry.getValue().toString(); + sb.append(tableInfo).append("\n\n"); + + // 自动分类表(简化版,可根据实际需求扩展) + if (tableName.startsWith("wms_")) { + tableCategories.computeIfAbsent("仓储管理相关表", k -> new ArrayList<>()).add(tableName); + } else if (tableName.startsWith("base_")) { + tableCategories.computeIfAbsent("基础信息表", k -> new ArrayList<>()).add(tableName); + } else if (tableName.startsWith("dms_") || tableName.contains("sys_user")) { + tableCategories.computeIfAbsent("设备管理相关表", k -> new ArrayList<>()).add(tableName); + } else if (tableName.startsWith("qc_") || tableName.contains("inspection")) { + tableCategories.computeIfAbsent("质量管理相关表", k -> new ArrayList<>()).add(tableName); + } else if (tableName.startsWith("prod_") || tableName.contains("log")) { + tableCategories.computeIfAbsent("MES相关表", k -> new ArrayList<>()).add(tableName); + } else if (tableName.startsWith("sys_")) { + tableCategories.computeIfAbsent("系统管理相关表", k -> new ArrayList<>()).add(tableName); + } else { + tableCategories.computeIfAbsent("其他表", k -> new ArrayList<>()).add(tableName); + } + }); + + // 添加表分类信息,帮助AI理解 + sb.append("【表分类参考】\n"); + tableCategories.forEach((category, tables) -> { + sb.append(category).append(": ").append(String.join(", ", tables)).append("\n"); + }); + sb.append("\n"); + + return sb.toString(); + } + + /** + * 构建AI提示词 + */ + private String buildAIPrompt(String schemaDescription, String naturalLanguageQuery) { + return String.format(""" + 你是一个专业的SQL Server数据库专家,需要根据用户查询意图和提供的表结构,智能选择最合适的表和字段生成SQL。 + + # 核心规则 + 1. **查询意图分析**: + - 分析用户查询的自然语言,识别关键词 + - 根据关键词选择最相关的表 + + 2. **表选择策略**: + - 如果查询意图明确(如"库存"、"物料"、"用户"),选择对应的核心表 + - 如果查询需要关联信息,进行必要的JOIN + - 优先选择包含核心业务数据的表 + + 3. **字段选择原则**: + - 选择与查询意图最相关的字段 + - 包含必要的标识字段(如ID、编码) + - 使用有意义的字段别名(中文优先) + + 4. **SQL规范**: + - 使用SQL Server特有语法(TOP、WITH(NOLOCK)) + - 使用ANSI JOIN语法 + - 默认添加合理的时间排序:ORDER BY create_time DESC 或 update_time DESC + - 如果表有状态字段(status),默认只查询正常状态的数据 + - 如果表有删除标记(del_flag),默认过滤已删除的数据(del_flag = '0');注意有的表没有del_flag + - 不要添加未明确要求的过滤条件 + - 每个字段必须有AS别名 + - 每个表必须有表别名 + - 在JOIN和WHERE条件中必须使用表别名.字段名格式 + + + 5. **性能优化**: + - 添加WITH(NOLOCK)提示(适用于高并发环境) + - 使用TOP限制结果集(默认TOP 1000) + - 在JOIN条件字段上应该建立索引 + + # 数据库表结构 + %s + + # 用户查询 + %s + + # 生成要求 + 基于以上规则,请: + 1. 分析用户查询意图 + 2. 选择最合适的表和字段 + 3. 生成优化后的T-SQL语句 + 4. 只返回SQL语句,不要任何解释 + + 请生成SQL:""", schemaDescription, naturalLanguageQuery); + } + + /** + * 处理AI响应 + */ + private String handleAIResponse(Mono response, AIRequest aiRequest, + IUnifiedAIProviderProcessor processor, String prompt) { + AIResponse aiResponse = response.block(); + if (aiResponse == null) { + throw new RuntimeException("AI服务无响应"); + } + + if (!aiResponse.isSuccess()) { + throw new RuntimeException("生成SQL语句失败:" + aiResponse.getErrorMessage()); + } + + String content = aiResponse.getContent().toString(); + + // 提取SQL语句 + String sqlContent = extractSqlFromContent(content); + + // 保存token使用记录 + try { + processor.saveTokenUsage( + aiRequest.getMessageDetailType(), + prompt, + sqlContent, + aiResponse.getTokenUsage(), + aiRequest.getModelId(), + null, + null, + null, + aiRequest.getSessionId(), + "0", + "1", + LoginHelper.getUserId(), + LoginHelper.getTenantId(), + LoginHelper.getDeptId() + ); + } catch (Exception e) { + throw new RuntimeException("保存token使用记录失败" + e.getMessage()); + } + + return sqlContent; + } /** @@ -354,37 +908,120 @@ public class AIAssistantServiceImpl implements IAIAssistantService { } - // 专门处理 Markdown SQL 代码块 - private static String extractSqlFromContent(String content) { - // 情况1:如果包含 ```sql ... ``` 格式 - if (content.contains("```sql")) { - int start = content.indexOf("```sql") + 6; // 跳过 ```sql - int end = content.lastIndexOf("```"); + /** + * 从AI响应内容中提取SQL语句 + */ + private String extractSqlFromContent(String content) { + if (StringUtils.isBlank(content)) { + return ""; +// return "SELECT 1 AS Error WHERE 1=0; -- 未生成SQL"; + } + + // 清理内容 + String cleanedContent = content.trim(); + + // 1. 如果包含 ```sql ``` 标记,提取中间内容 + if (cleanedContent.contains("```sql")) { + int start = cleanedContent.indexOf("```sql") + 6; + int end = cleanedContent.lastIndexOf("```"); if (end > start) { - content = content.substring(start, end).trim(); + cleanedContent = cleanedContent.substring(start, end).trim(); } } - // 情况2:如果只有 ``` ... ```(没有 sql 标注) - if (content.contains("```")) { - int start = content.indexOf("```") + 3; - int end = content.lastIndexOf("```"); + // 2. 如果包含 ``` 标记(无语言标识),提取中间内容 + if (cleanedContent.contains("```")) { + int start = cleanedContent.indexOf("```") + 3; + int end = cleanedContent.lastIndexOf("```"); if (end > start) { - content = content.substring(start, end).trim(); + cleanedContent = cleanedContent.substring(start, end).trim(); } } - // content = extractSqlFromText(content); - content = content.replace("\\n", "\n") // 处理转义的换行符 - .replace("\n", " "); + // 3. 移除可能的SQL解释或说明文本 + String[] lines = cleanedContent.split("\n"); + StringBuilder sqlBuilder = new StringBuilder(); + boolean sqlStarted = false; - content = content.trim(); - if ((content.startsWith("\"") && content.endsWith("\"")) || - (content.startsWith("'") && content.endsWith("'"))) { - content = content.substring(1, content.length() - 1); + for (String line : lines) { + String trimmedLine = line.trim(); + + // 跳过空行和明显的非SQL行 + if (trimmedLine.isEmpty() || + trimmedLine.startsWith("--") || + trimmedLine.startsWith("/*") || + trimmedLine.startsWith("解释") || + trimmedLine.startsWith("说明") || + trimmedLine.startsWith("分析") || + trimmedLine.toLowerCase().startsWith("here") || + trimmedLine.toLowerCase().startsWith("this sql") || + trimmedLine.toLowerCase().startsWith("the sql")) { + continue; + } + + // 如果遇到SELECT/INSERT/UPDATE/DELETE,开始收集 + if (!sqlStarted && + (trimmedLine.toUpperCase().startsWith("SELECT") || + trimmedLine.toUpperCase().startsWith("WITH") || + trimmedLine.toUpperCase().startsWith("INSERT") || + trimmedLine.toUpperCase().startsWith("UPDATE") || + trimmedLine.toUpperCase().startsWith("DELETE"))) { + sqlStarted = true; + } + + if (sqlStarted) { + sqlBuilder.append(line).append("\n"); + } } - return content.trim(); + String finalSql = sqlBuilder.toString().trim(); + + // 4. 确保SQL以分号结束 + if (!finalSql.endsWith(";")) { + finalSql += ";"; + } + + // 5. 如果是空SQL,返回安全查询 + if (StringUtils.isBlank(finalSql) || finalSql.equals(";")) { + return ""; +// return "SELECT 1 AS NoSQLGenerated WHERE 1=0; -- 未能生成有效SQL"; + } + + return finalSql; + } + + + /** + * 可选:添加SQL验证方法(可选增强) + */ + private boolean validateGeneratedSQL(String sql) { + if (StringUtils.isBlank(sql)) { + return false; + } + + // 基础验证 + String upperSql = sql.toUpperCase(); + + // 检查是否包含潜在危险操作 + if (upperSql.contains("DROP ") || + upperSql.contains("TRUNCATE ") || + upperSql.contains("ALTER ") || + upperSql.contains("CREATE ") || + upperSql.contains("EXEC ") || + upperSql.contains("EXECUTE ") || + upperSql.contains("SP_") || + upperSql.contains("XP_")) { +// log.warn("生成的SQL可能包含危险操作: {}", sql.substring(0, Math.min(sql.length(), 100))); + return false; + } + + // 检查是否是有效的SELECT查询(如果是查询场景) + if (!upperSql.contains("SELECT")) { +// log.warn("生成的SQL不是SELECT查询: {}", sql.substring(0, Math.min(sql.length(), 100))); + return false; + } + + return true; } @@ -407,34 +1044,38 @@ public class AIAssistantServiceImpl implements IAIAssistantService { // } - String naturalLanguageQuery = aiFillFormRequest.getNaturalLanguageQuery()+"."; + String naturalLanguageQuery = aiFillFormRequest.getNaturalLanguageQuery(); List aiFormSettingDetailList = aiFillFormRequest.getFormSettingDetailList(); - StringBuilder sb = new StringBuilder("你是一个智能表单填充助手。请根据我的要求、提供的数据库表结构信息,生成一份用于直接填充Vue3前端表单的JSON数据。\n\n"); - - sb.append("【要求】\n"); - sb.append(naturalLanguageQuery).append(",\n\n"); - - sb.append("【数据库表信息】\n"); - sb.append(getFormattedFormSettingDetails(aiFormSettingDetailList)); - - sb.append("\n【输出要求】\n"); - sb.append("1. 请根据我的核心要求,智能推断并填充所有相关字段的值\n"); - sb.append("2. 生成一个纯净的JSON对象,不要任何额外的解释、文本或markdown代码块标记,键是字段名,值是根据我的要求推断出来的值\n"); -// sb.append("3. 请返回**纯净的JSON格式**数据,不要任何额外的解释、文本或markdown代码块标记\n"); - sb.append("3. 仅返回一个JSON对象,不要返回JSON数组,不要返回任何测试数据和推测的数据\n"); - sb.append("4. 其中的表名和字段名必须来自数据库表结构信息\n"); - sb.append("5. 其他没有要求返回的数据返回空字符串\n"); + String prompt = buildAiFillFormPrompt( + naturalLanguageQuery, + aiFormSettingDetailList + ); +// StringBuilder sb = new StringBuilder("你是一个智能表单填充助手。请根据我的要求、提供的数据库表结构信息,生成一份用于直接填充Vue3前端表单的JSON数据。\n\n"); +// +// sb.append("【要求】\n"); +// sb.append(naturalLanguageQuery).append("\n\n"); +// +// sb.append("【数据库表信息】\n"); +// sb.append(getFormattedFormSettingDetails(aiFormSettingDetailList)); +// +// sb.append("\n【输出要求】\n"); +// sb.append("1. 请根据我的核心要求,智能推断并填充所有相关字段的值\n"); +// sb.append("2. 生成一个纯净的JSON对象,不要任何额外的解释、文本或markdown代码块标记,键是字段名,值是根据我的要求推断出来的值\n"); +//// sb.append("3. 请返回**纯净的JSON格式**数据,不要任何额外的解释、文本或markdown代码块标记\n"); +// sb.append("3. 仅返回一个JSON对象,不要返回JSON数组,不要返回任何测试数据和推测的数据\n"); +// sb.append("4. 其中的表名、字段名和条件中的字段名必须来自数据库表结构信息\n"); +// sb.append("5. 其他没有要求返回的数据返回空字符串\n"); IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory .getProcessorByPlatformId(aiFillFormRequest.getPlatformId()); AIMessage aiMessage = new AIMessage(); aiMessage.setRole("user"); - aiMessage.setContent(sb.toString()); + aiMessage.setContent(prompt); Long modelId = aiFillFormRequest.getModelId(); AIRequest aiRequest = new AIRequest(); aiRequest.setMessages(Collections.singletonList(aiMessage)); - aiRequest.setText(sb.toString()); + aiRequest.setText(prompt); aiRequest.setModelId(modelId); Mono response = processor.chat(aiRequest); @@ -442,9 +1083,9 @@ public class AIAssistantServiceImpl implements IAIAssistantService { String content = response.block().getContent().toString(); JSONObject contentJson = JSONObject.parseObject(content); parseRelateTable(aiFormSettingDetailList, contentJson); - processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_FORM,sb.toString(), content, response.block().getTokenUsage(), - modelId, null, null, - null, null, "0", "1",LoginHelper.getUserId(),LoginHelper.getTenantId(),LoginHelper.getDeptId()); + processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_FORM, prompt, content, response.block().getTokenUsage(), + modelId, null, null, + null, null, "0", "1", LoginHelper.getUserId(), LoginHelper.getTenantId(), LoginHelper.getDeptId()); System.out.println(contentJson.toJSONString()); return contentJson; @@ -455,6 +1096,32 @@ public class AIAssistantServiceImpl implements IAIAssistantService { } + /** + * 构建AI提示词 + */ + private String buildAiFillFormPrompt(String naturalLanguageQuery, + List formDetails) { + StringBuilder sb = new StringBuilder(); + sb.append("你是一个智能表单填充助手。请根据以下要求填充表单:\n\n"); + + sb.append("【用户需求】\n") + .append(naturalLanguageQuery) + .append("\n\n"); + + sb.append("【表单结构】\n") + .append(getFormattedFormSettingDetails(formDetails)) + .append("\n\n"); + + sb.append("【输出要求】\n") + .append("1. 根据需求推断并填充所有相关字段的值\n") + .append("2. 仅返回一个JSON对象,不要任何额外文本\n") + .append("3. 键名必须来自表单结构中的字段名\n") + .append("4. 未指定的字段返回空字符串\n") + .append("5. 不要随意添加标点符号"); + + return sb.toString(); + } + private void parseRelateTable(List aiFormSettingDetailList, JSONObject contentJson) { for (AiFormSettingDetail aiFormSettingDetail : aiFormSettingDetailList) { if (aiFormSettingDetail.getSettingFlag().equals("1")) { diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIReportServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIReportServiceImpl.java new file mode 100644 index 00000000..d31da30c --- /dev/null +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AIReportServiceImpl.java @@ -0,0 +1,1964 @@ +package org.dromara.ai.service.impl; + +/** + * @Author xins + * @Date 2026/1/5 14:41 + * @Description:AI报表分析服务实现类 + */ + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.yulichang.toolkit.JoinWrappers; +import com.github.yulichang.wrapper.MPJLambdaWrapper; +import jakarta.servlet.http.HttpServletResponse; +import org.dromara.ai.domain.AiChatMessage; +import org.dromara.ai.domain.AiChatMessageDetail; +import org.dromara.ai.domain.bo.AiChatMessageBo; +import org.dromara.ai.domain.bo.AiChatMessageDetailBo; +import org.dromara.ai.domain.dto.AIReportResult; +import org.dromara.ai.domain.dto.AIReportRequest; +import org.dromara.ai.domain.dto.AIReportResponse; +import org.dromara.ai.domain.vo.AiChatMessageDetailVo; +import org.dromara.ai.domain.vo.AiChatMessageVo; +import org.dromara.ai.mapper.AiChatMessageDetailMapper; +import org.dromara.ai.mapper.AiChatMessageMapper; +import org.dromara.ai.mapper.SQLServerDatabaseMetaMapper; +import org.dromara.ai.process.dto.AIMessage; +import org.dromara.ai.process.dto.AIRequest; +import org.dromara.ai.process.provider.processor.AIProviderProcessorFactory; +import org.dromara.ai.process.provider.processor.IUnifiedAIProviderProcessor; +import org.dromara.ai.service.IAIReportService; +import org.dromara.common.constant.HwMomAiConstants; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.common.json.utils.JsonUtils; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.system.api.model.LoginUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +public class AIReportServiceImpl implements IAIReportService { + + private static final Random RANDOM = ThreadLocalRandom.current(); + + // 默认时间格式 + private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + // 常用时间格式 + public static final String FORMAT_DATE_ONLY = "yyyy-MM-dd"; + public static final String FORMAT_DATETIME = "yyyy-MM-dd HH:mm:ss"; + public static final String FORMAT_DATETIME_MS = "yyyy-MM-dd HH:mm:ss.SSS"; + public static final String FORMAT_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + + @Autowired + private SQLServerDatabaseMetaMapper databaseMetaMapper; + + @Autowired + private AIProviderProcessorFactory aiProviderProcessorFactory; + + @Autowired + private AiChatMessageMapper aiChatMessageMapper; + + @Autowired + private AiChatMessageDetailMapper aiChatMessageDetailMapper; + + @Autowired + private ObjectMapper objectMapper; + + + public Mono generateReport(String provider, AIRequest aiRequest) { + // 创建final副本 + final AIRequest finalAiRequest = aiRequest; + final LoginUser loginUser = LoginHelper.getLoginUser(); + + // 提前获取messages并进行验证 + final List messages = finalAiRequest.getMessages(); + if (messages == null || messages.isEmpty()) { +// return Mono.just(createErrorResponse("消息列表不能为空")); + } + + // 1. 查询数据库获取数据 +// String sql1 = "select * from wms_inventory"; + List> reportData = new ArrayList<>(); + String reportSql = ""; + try { + reportSql = aiRequest.getSql(); + reportData = databaseMetaMapper.dynamicSelectSql(reportSql); + if (reportData == null || reportData.isEmpty()) { + throw new RuntimeException("未查询到数据[" + reportSql + "]"); + } + } catch (Exception e) { + //todo:删除或修改状态为删除,sql语句不能用 + throw new RuntimeException("生成SQL有误[" + reportSql + "]"); + } + + String sessionId = aiRequest.getSessionId(); + AiChatMessageDetailBo aiChatMessageDetailBo = new AiChatMessageDetailBo(); + aiChatMessageDetailBo.setSessionId(sessionId); + aiChatMessageDetailBo.setMessageDetailType(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT); + AiChatMessageDetailVo aiChatMessageDetailVo = getRecentReportInfo(aiChatMessageDetailBo); + + List> finalReportData = reportData; + if (aiChatMessageDetailVo != null && StringUtils.isNotBlank(aiChatMessageDetailVo.getAnswerContent())) { + AIReportResponse aiReportResponse = new AIReportResponse(); + aiReportResponse.setData(finalReportData); + aiReportResponse.setChartHtml(aiChatMessageDetailVo.getAnswerContent()); + return Mono.just(aiReportResponse); + } else { + // 2. 构建详细的提示词 + StringBuilder sb = new StringBuilder(); + sb.append("你是一个智能报表助手。请根据我的要求、提供的数据信息,生成一份直接可以运行预览的HTML数据。\n\n"); +// sb.append("返回一段简单的柱状图,直接可以运行的HTML数据"); + + sb.append("【要求】\n"); + sb.append("1. 请基于以下数据生成分析报表:\n"); + sb.append(JsonUtils.toJsonString(reportData)); + sb.append("\n\n"); + + sb.append("2. 报表需要包含以下内容:\n"); + sb.append(" - 总览\n"); + sb.append(" - 图表展示(分布、趋势等)\n"); + sb.append(" - 详细数据表格\n\n"); +// + sb.append("【输出要求】\n"); + sb.append("1. 生成一个完整的、独立的HTML页面\n"); + sb.append("2. HTML内容以开头,结束\n"); + sb.append("3. 页面包含CSS样式,确保美观\n"); + sb.append("4. 使用ECharts生成图表,但不要引用外部JS(父页面已引用),在内的第一行固定写如下代码:let echarts = window.parent.echarts;然后换行\n"); + sb.append("5. 图表或列表引用的数据不需要再返回,直接引用父页面的数据,在中的第二行固定写如下代码: const rawData = window.parent.reportData;\n"); +// sb.append("6. 在加载遍历数据rawData时,不要用item.key,用item[key]\n"); + sb.append("6. 不要包含任何额外的解释、注释、文本或markdown代码块标记\n"); + sb.append("7. 不要有换行,只返回一行代码\n"); + sb.append("8. 确保HTML可以直接在浏览器中运行\n"); + +// sb.append("\n【数据说明】\n"); +// sb.append("这是WMS库存数据,包含物料编码、物料名称、库存数量、单位、仓库位置等信息。\n"); + + sb.append("\n【示例格式】\n"); + sb.append(""" + + + + + +

分析报表

+
+
+

详细数据

+ ...
+ + + + """); + + // 3. 准备AI请求 + String prompt = sb.toString(); + final String messageTopic = messages.get(messages.size() - 1).getContent(); + + finalAiRequest.setMessageTopic(messageTopic); + finalAiRequest.setQuestionContent(prompt); + + AIMessage aiMessage = new AIMessage(); + aiMessage.setRole("user"); + aiMessage.setContent(prompt); + finalAiRequest.setMessages(Collections.singletonList(aiMessage)); + + // 设置合适的参数 + if (finalAiRequest.getTemperature() == null) { + finalAiRequest.setTemperature(0.7); // 适中的创造性 + } + if (finalAiRequest.getMaxTokens() == null) { + finalAiRequest.setMaxTokens(8000); // 增加token限制以容纳HTML + } + + // 4. 调用AI并收集完整响应 + + return Mono.defer(() -> { + try { + IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory.getProcessor(provider); + AIReportResponse aiReportResponse = new AIReportResponse(); + aiReportResponse.setData(finalReportData); + + // 使用cache()确保流内容只计算一次 + Flux cachedStream = processor.chatStreamComplete(finalAiRequest, loginUser) + .map(content -> content != null ? content.getContent() : "") + .cache(); // 关键:缓存流内容 + + // 收集数据 + return cachedStream + .collect(StringBuilder::new, StringBuilder::append) + .map(fullContentBuilder -> { + String fullContent = fullContentBuilder.toString(); +// String processedHtml = processAndValidateHtml(fullContent); + + +// System.out.println(fullContent); + aiReportResponse.setChartHtml(fullContent); +// return buildSuccessAIReportResponse(processedHtml, sql1, testData, prompt); + return aiReportResponse; + }) + .timeout(Duration.ofSeconds(600)) + .onErrorResume(throwable -> { + System.out.println("生成报表失败: " + throwable.getMessage()); + throwable.printStackTrace(); +// return Mono.just(createErrorAIReportResponse("生成报表失败: " + throwable.getMessage())); + return null; + }); + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("#123222222"); +// log.error("生成报表时发生异常", e); + throw new RuntimeException(""); + } + }); + } + + } + + + /** + * 获取最近5条AI生成报表的信息 + * + * @return List + */ + @Override + public TableDataInfo getRecentReports() { + MPJLambdaWrapper lqw = JoinWrappers.lambda(AiChatMessage.class) + .selectAll(AiChatMessage.class) + .eq(AiChatMessage::getMessageType, HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT) + .orderByDesc(AiChatMessage::getChatMessageId); + + PageQuery pageQuery = new PageQuery(); + pageQuery.setPageNum(1); + pageQuery.setPageSize(5); + Page aiChatMessageVos = aiChatMessageMapper.selectPage(pageQuery.build(), lqw); + return TableDataInfo.build(aiChatMessageVos); + + } + + + /** + * 根据sessionid和messagedetailtype获取一条最新的AI生成报表详细信息 + * + * @param aiChatMessageDetailBo + * @return AiChatMessageDetailVo + */ + @Override + public AiChatMessageDetailVo getRecentReportInfo(AiChatMessageDetailBo aiChatMessageDetailBo) { + MPJLambdaWrapper lqw = JoinWrappers.lambda(AiChatMessageDetail.class) + .selectAll(AiChatMessageDetail.class) + .eq(AiChatMessageDetail::getMessageDetailType, aiChatMessageDetailBo.getMessageDetailType()) + .eq(AiChatMessageDetail::getSessionId, aiChatMessageDetailBo.getSessionId()) + .orderByDesc(AiChatMessageDetail::getMessageDetailId); + + List aiChatMessageDetailVos = aiChatMessageDetailMapper.selectVoList(lqw); + + AiChatMessageDetailVo aiChatMessageDetailVo = aiChatMessageDetailVos != null && !aiChatMessageDetailVos.isEmpty() + ? aiChatMessageDetailVos.get(0) : new AiChatMessageDetailVo(); + + return aiChatMessageDetailVo; + + } + + + // 辅助方法:处理并验证HTML + private String processAndValidateHtml(String html) { + if (html == null || html.trim().isEmpty()) { + return "

报表内容为空

"; + } + + // 清理可能的markdown代码块 + String cleaned = html.replaceAll("```html", "") + .replaceAll("```", "") + .trim(); + + // 确保以开头 + if (!cleaned.startsWith(""; + } + + // 确保有完整的HTML结构 + if (!cleaned.contains("")) { + cleaned = cleaned.replaceFirst("", + "库存分析报表"); + } + + // 添加基础样式 + if (!cleaned.contains(" + """); + } + + return cleaned; + } + + + // 辅助方法:创建错误响应 + private String createErrorResponse(String errorMessage) { + AIReportResult result = new AIReportResult(); + result.setSuccess(false); + result.setCode("ERROR"); + result.setMessage(errorMessage); + return JsonUtils.toJsonString(result); + } + + + /** + * 导出AI生成报表的数据(excel格式) + * + * @param aiRequest + * @param response + */ + @Override + public void exportReportData(AIRequest aiRequest, HttpServletResponse response) { + String reportSql = aiRequest.getSql(); + List> reportData = new ArrayList<>(); +// try { + reportData = databaseMetaMapper.dynamicSelectSql(reportSql); + if (reportData == null || reportData.isEmpty()) { + throw new RuntimeException("未查询到数据[" + reportSql + "]"); + } + + // 1. 获取所有列名(保持顺序) + List allColumns = getAllColumns(reportData, null); + + // 2. 构建表头 + List> headers = buildHeaders(allColumns, null); + + // 3. 构建数据 + SimpleDateFormat sdf = new SimpleDateFormat(DEFAULT_DATE_FORMAT); + List> data = buildDataWithTimestampConversion(reportData, allColumns, sdf); + + ExcelUtil.exportExcelWithDynamicHead(data, headers, aiRequest.getText(), response); + +// } catch (Exception e) { +// e.printStackTrace(); +// throw new RuntimeException("导出数据有误[" + e + "]"); +// } + + } + + + /** + * 获取所有列名 + */ + private static List getAllColumns(List> mapList, + List columnOrder) { + // 使用LinkedHashSet保持顺序并去重 + Set columnSet = new LinkedHashSet<>(); + + // 如果指定了列顺序,按指定顺序添加 + if (columnOrder != null && !columnOrder.isEmpty()) { + columnSet.addAll(columnOrder); + } + + // 添加所有Map中存在的key + for (Map map : mapList) { + if (map != null) { + columnSet.addAll(map.keySet()); + } + } + + return new ArrayList<>(columnSet); + } + + /** + * 构建表头 + */ + private static List> buildHeaders(List columns, + Map headerMapping) { + List> headers = new ArrayList<>(); + + // 单行表头 + List headerRow = new ArrayList<>(); + for (String column : columns) { + // 如果有表头映射,使用映射后的名称,否则使用原始列名 + String headerName = headerMapping != null && headerMapping.containsKey(column) + ? headerMapping.get(column) + : column; + headers.add(Collections.singletonList(headerName)); + } + + + // 如果需要多级表头,可以在这里添加更多行 + // headers.add(secondHeaderRow); + + return headers; + } + + /** + * 构建数据 + */ + private static List> buildData(List> mapList, + List columns) { + List> data = new ArrayList<>(); + + for (Map map : mapList) { + List row = new ArrayList<>(); + + for (String column : columns) { + Object value = map.get(column); + + // 处理嵌套对象(支持点号分隔的路径) + if (value == null && column.contains(".")) { + value = getNestedValue(map, column); + } + + // 处理特殊值 + if (value == null) { + row.add(""); + } else if (value instanceof Date) { + row.add(value); // 保持Date对象,前端可以格式化 + } else { + row.add(value); + } + } + + data.add(row); + } + + return data; + } + + + /** + * 构建数据(转换时间戳为字符串) + */ + private static List> buildDataWithTimestampConversion( + List> mapList, + List columns, + SimpleDateFormat dateFormat) { + + List> data = new ArrayList<>(); + + for (Map map : mapList) { + List row = new ArrayList<>(); + + for (String column : columns) { + Object value = map.get(column); + + // 转换时间戳 + value = convertTimestampToString(value, dateFormat); + + // 处理null值 + if (value == null) { + row.add(""); + } else { + row.add(value); + } + } + + data.add(row); + } + + return data; + } + + /** + * 转换时间戳对象为字符串 + */ + private static Object convertTimestampToString(Object value, SimpleDateFormat dateFormat) { + if (value == null) { + return null; + } + + // 处理Timestamp类型 + if (value instanceof Timestamp) { + Timestamp timestamp = (Timestamp) value; + return dateFormat.format(new Date(timestamp.getTime())); + } + + // 处理java.util.Date类型 + if (value instanceof Date) { + Date date = (Date) value; + return dateFormat.format(date); + } + + // 处理java.sql.Date类型 + if (value instanceof java.sql.Date) { + java.sql.Date sqlDate = (java.sql.Date) value; + return dateFormat.format(new Date(sqlDate.getTime())); + } + + // 其他类型保持原样 + return value; + } + + + /** + * 获取嵌套对象的值 + */ + private static Object getNestedValue(Map map, String path) { + if (path == null || !path.contains(".")) { + return map.get(path); + } + + String[] keys = path.split("\\."); + Object value = map; + + for (String key : keys) { + if (value instanceof Map) { + value = ((Map) value).get(key); + } else { + return null; + } + } + + return value; + } + + /** + * 流式聊天接口 + * + * @param provider + * @param aiRequest + * @return Flux + */ + @Override + public Flux streamGenerateReport(String provider, AIRequest aiRequest) { + // 创建final副本 + final AIRequest finalAiRequest = aiRequest; + final LoginUser loginUser = LoginHelper.getLoginUser(); + + // 提前获取messages并进行验证 + final List messages = finalAiRequest.getMessages(); + if (messages == null || messages.isEmpty()) { + return Flux.just(createErrorEvent(new IllegalArgumentException("消息列表不能为空"))); + } + + String sql1 = "select * from wms_inventory"; + + List> testData = databaseMetaMapper.dynamicSelectSql(sql1); + StringBuilder sb = new StringBuilder(); + sb.append("你是一个智能报表助手。请根据我的要求、提供的数据信息,生成一份直接可以运行预览的HTML数据。\n\n"); + + sb.append("【要求】\n"); + sb.append("1. 请基于以下库存数据生成分析报表:\n"); + sb.append(JsonUtils.toJsonString(testData)); + sb.append("\n\n"); + + sb.append("2. 报表需要包含以下内容:\n"); + sb.append(" - 库存总览(物料总数、总库存量、平均库存等)\n"); + sb.append(" - 物料分类统计\n"); + sb.append(" - 库存量TOP10的物料\n"); + sb.append(" - 图表展示(库存分布、趋势等)\n"); + sb.append(" - 详细数据表格\n\n"); + + sb.append("【输出要求】\n"); + sb.append("1. 生成一个完整的、独立的HTML页面\n"); + sb.append("2. HTML内容以开头,结束\n"); + sb.append("3. 页面包含CSS样式,确保美观\n"); + sb.append("4. 使用ECharts生成图表,但不要引用外部JS(父页面已引用),在内的第一行固定写如下代码:let echarts = window.parent.echarts\n"); + sb.append("5. 不要包含任何额外的解释、文本或markdown代码块标记\n"); + sb.append("6. 确保HTML可以直接在浏览器中运行\n"); + + sb.append("\n【数据说明】\n"); +// sb.append("这是WMS库存数据,包含物料编码、物料名称、库存数量、单位、仓库位置等信息。\n"); + + sb.append("\n【示例格式】\n"); + sb.append(""" + + + + + +

库存分析报表

+
+
+

详细数据

+ ...
+ + + + """); + + + String prompt = "以下是库存数据列表:" + testData.toString() + ",请返回一段完整的HTML字符串分析物料库存,返回图表和列表,html内容以开头,结束"; + + + final String messageTopic = messages.get(messages.size() - 1).getContent(); + finalAiRequest.setMessageTopic(messageTopic);//暂时跟提问的内容相同,后续可以总结 + finalAiRequest.setQuestionContent(prompt); + AIMessage aiMessage = new AIMessage(); + aiMessage.setRole("user"); + aiMessage.setContent(sb.toString()); + finalAiRequest.setMessages(Collections.singletonList(aiMessage)); + + // 尝试获取处理器,如果找不到则抛出异常 + return Flux.defer(() -> { + try { + AIRequest processedRequest = finalAiRequest; + + IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory.getProcessor(provider); + + return processor.chatStream(processedRequest, loginUser) + .onErrorResume(throwable -> { + return Flux.just(createErrorEvent(throwable)); + }); + + } catch (Exception e) { + return Flux.just(createErrorEvent(e)); + } + }); + } + + + @Override + public AIReportResponse generateTestReport(AIReportRequest request) { + String sql1 = "select * from wms_inventory"; + + List> testData = databaseMetaMapper.dynamicSelectSql(sql1); + String prompt = "以下是库存数据列表:" + testData.toString() + ",请返回一段完整的HTML字符串分析物料库存,可以直接加载到VUE3文件中显示echarts柱状图图表。"; + + AIRequest aiRequest = new AIRequest(); + aiRequest.setPlatformId(1L); + aiRequest.setModelId(1L); + IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory.getProcessorByPlatformId(aiRequest.getPlatformId()); + AIMessage aiMessage = new AIMessage(); + aiMessage.setRole("user"); + aiMessage.setContent(prompt); + aiRequest.setMessages(Collections.singletonList(aiMessage)); + + AIReportResponse response = new AIReportResponse(); + + String contentt = "\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " 物料库存分析系统\n" + +// " \n" + + " \n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "

物料库存分析系统

\n" + + "
实时库存监控与数据分析
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\uD83D\uDCCA
\n" + + "
0
\n" + + "
总库存数量
\n" + + "
\n" + + "
\n" + + "
\uD83D\uDCE6
\n" + + "
0
\n" + + "
物料种类
\n" + + "
\n" + + "
\n" + + "
\uD83C\uDFF7\uFE0F
\n" + + "
0
\n" + + "
批次总数
\n" + + "
\n" + + "
\n" + + "
\uD83D\uDCCD
\n" + + "
0
\n" + + "
库位数量
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + "
\uD83D\uDCC8 物料库存量柱状图
\n" + + "
\n" + + "
\n" + + "
\n" + + "
\uD83E\uDD67 库存分布饼图
\n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "
\uD83D\uDCCB 库存明细数据表
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
物料编码物料ID库存数量批次号库位创建时间状态
\n" + + "
\n" + + "
\n" + + " \n" + + "
\n" + + "

数据更新时间: 2025-09-19 | 仓库ID: 4 | 租户: 000000 | 共7条库存记录

\n" + + "
\n" + + "
\n" + + "\n" + + " \n" + + "\n" + + ""; + + response.setChartHtml(contentt); +// try { +// Mono response1 = processor.chat(aiRequest); +// if (Objects.requireNonNull(response1.block()).isSuccess()) { +// String content = response1.block().getContent().toString(); +// System.out.println("----------:"+content); +// +// content = extractHtmlContent(content); +// +// System.out.println("--:" + content); +// response.setChartHtml(content); +// } else { +// throw new RuntimeException("生成sql语句失败" + response1.block().getErrorMessage()); +// } +// +// }catch (Exception e){ +// e.printStackTrace(); +// } + + + System.out.println(testData.toString()); + + long startTime = System.currentTimeMillis(); + + // 1. 模拟AI生成SQL + String sql = generateSQL(request.getPrompt()); + + // 2. 执行SQL查询数据(模拟) + List> data = executeSQLMock(sql); + + // 3. 获取字段信息 + List fields = getFieldInfo(data); + + // 4. 模拟AI生成HTML + String chartHtml = generateChartHTML(request.getPrompt(), data, fields); + String tableHtml = generateTableHTML(request.getPrompt(), data, fields); + + // 5. 生成分析建议 + String analysis = generateAnalysis(request.getPrompt(), data); + + long executionTime = System.currentTimeMillis() - startTime; + + + response.setSql(sql); + response.setData(data); + response.setFields(fields); + + response.setTableHtml(tableHtml); + response.setAnalysis(analysis); + response.setExecutionTime(executionTime); + + return response; + } + + @Override + public String generateSQL(String prompt) { + String lowerPrompt = prompt.toLowerCase(); + + if (lowerPrompt.contains("top10") || lowerPrompt.contains("top 10")) { + if (lowerPrompt.contains("物料") && lowerPrompt.contains("库存")) { + return "SELECT material_name, stock_quantity, stock_value, supplier_name " + + "FROM materials m " + + "JOIN suppliers s ON m.supplier_id = s.supplier_id " + + "ORDER BY stock_quantity DESC LIMIT 10"; + } else if (lowerPrompt.contains("销售")) { + return "SELECT product_name, sales_quantity, sales_amount, region " + + "FROM sales s " + + "JOIN products p ON s.product_id = p.product_id " + + "ORDER BY sales_quantity DESC LIMIT 10"; + } + } else if (lowerPrompt.contains("月度") && lowerPrompt.contains("销售")) { + return "SELECT DATE_FORMAT(sale_date, '%Y-%m') as month, " + + "SUM(sales_amount) as total_amount, " + + "SUM(sales_quantity) as total_quantity " + + "FROM sales " + + "GROUP BY DATE_FORMAT(sale_date, '%Y-%m') " + + "ORDER BY month DESC LIMIT 12"; + } else if (lowerPrompt.contains("库存") && lowerPrompt.contains("预警")) { + return "SELECT material_name, stock_quantity, min_stock_level, " + + "(stock_quantity - min_stock_level) as shortage " + + "FROM materials " + + "WHERE stock_quantity < min_stock_level " + + "ORDER BY shortage ASC"; + } + + // 默认返回物料库存SQL + return "SELECT material_name, stock_quantity, stock_value, supplier_name " + + "FROM materials m " + + "JOIN suppliers s ON m.supplier_id = s.supplier_id " + + "ORDER BY stock_quantity DESC LIMIT 10"; + } + + /** + * 模拟执行SQL查询数据 + */ + private List> executeSQLMock(String sql) { + List> data = new ArrayList<>(); + + if (sql.contains("materials") && sql.contains("stock_quantity")) { + // 模拟物料库存数据 + String[] materials = {"钢铁", "铜材", "铝材", "塑料粒子", "橡胶", "玻璃", "木材", "纸张", "水泥", "化工原料"}; + String[] suppliers = {"上海钢铁公司", "华北铜业", "南方铝业", "华东化工", "华北橡胶", "南方玻璃", "华东木材", "华北纸张", "南方水泥", "华东化工"}; + + for (int i = 0; i < 10; i++) { + Map row = new HashMap<>(); + row.put("material_name", materials[i]); + row.put("stock_quantity", RANDOM.nextInt(10000) + 1000); + row.put("stock_value", RANDOM.nextInt(1000000) + 50000); + row.put("supplier_name", suppliers[i]); + data.add(row); + } + } else if (sql.contains("sales")) { + // 模拟销售数据 + String[] products = {"产品A", "产品B", "产品C", "产品D", "产品E", "产品F", "产品G", "产品H", "产品I", "产品J"}; + String[] regions = {"华北", "华东", "华南", "西南", "东北"}; + + for (int i = 0; i < 10; i++) { + Map row = new HashMap<>(); + row.put("product_name", products[i]); + row.put("sales_quantity", RANDOM.nextInt(1000) + 100); + row.put("sales_amount", RANDOM.nextInt(100000) + 10000); + row.put("region", regions[RANDOM.nextInt(regions.length)]); + data.add(row); + } + } else if (sql.contains("month")) { + // 模拟月度数据 + for (int i = 11; i >= 0; i--) { + Map row = new HashMap<>(); + String month = String.format("2024-%02d", i + 1); + row.put("month", month); + row.put("total_amount", RANDOM.nextInt(500000) + 100000); + row.put("total_quantity", RANDOM.nextInt(10000) + 1000); + data.add(row); + } + } + + return data; + } + + /** + * 获取字段信息 + */ + private List getFieldInfo(List> data) { + if (data.isEmpty()) return new ArrayList<>(); + + List fields = new ArrayList<>(); + Map firstRow = data.get(0); + + for (String key : firstRow.keySet()) { + AIReportResponse.FieldInfo field = new AIReportResponse.FieldInfo(); + field.setName(key); + + Object value = firstRow.get(key); + if (value instanceof Number) { + field.setType("numeric"); + field.setNumeric(true); + } else { + field.setType("string"); + field.setNumeric(false); + } + + field.setComment(getFieldComment(key)); + field.setPrimary(false); + fields.add(field); + } + + return fields; + } + + /** + * 生成字段注释 + */ + private String getFieldComment(String fieldName) { + Map comments = new HashMap<>(); + comments.put("material_name", "物料名称"); + comments.put("stock_quantity", "库存数量"); + comments.put("stock_value", "库存价值"); + comments.put("supplier_name", "供应商名称"); + comments.put("product_name", "产品名称"); + comments.put("sales_quantity", "销售数量"); + comments.put("sales_amount", "销售金额"); + comments.put("region", "销售区域"); + comments.put("month", "月份"); + comments.put("total_amount", "总金额"); + comments.put("total_quantity", "总数量"); + comments.put("min_stock_level", "最低库存"); + comments.put("shortage", "缺货数量"); + + return comments.getOrDefault(fieldName, fieldName); + } + + /** + * 生成图表HTML + */ + private String generateChartHTML(String prompt, List> data, List fields) { + if (data.isEmpty()) return ""; + + StringBuilder html = new StringBuilder(); + html.append("
"); + html.append("

数据可视化图表

"); + html.append("
"); + + // 根据数据类型选择图表类型 + boolean isTop10 = prompt.toLowerCase().contains("top10") || prompt.toLowerCase().contains("top 10"); + boolean hasAmount = hasField(fields, "sales_amount") || hasField(fields, "stock_value") || hasField(fields, "total_amount"); + + if (isTop10 && hasAmount) { + // 生成柱状图模拟HTML + html.append(generateBarChartHTML(data, fields)); + } else if (hasField(fields, "month")) { + // 生成折线图模拟HTML + html.append(generateLineChartHTML(data, fields)); + } else { + // 生成饼图模拟HTML + html.append(generatePieChartHTML(data, fields)); + } + + html.append("
"); + html.append("
"); + html.append(""); + html.append("
"); + html.append("
"); + + return html.toString(); + } + + /** + * 生成表格HTML(支持复合表头) + */ + private String generateTableHTML(String prompt, List> data, List fields) { + if (data.isEmpty()) return ""; + + StringBuilder html = new StringBuilder(); + html.append("
"); + html.append("

详细数据列表

"); + html.append("
"); + html.append(""); + + // 表头 + html.append(""); + html.append(""); + + // 检查是否需要复合表头 + boolean isComplexTable = prompt.toLowerCase().contains("复合") || prompt.toLowerCase().contains("复杂") || + (hasField(fields, "sales_amount") && hasField(fields, "sales_quantity")); + + if (isComplexTable) { + html.append(generateComplexTableHeader(fields)); + } else { + for (AIReportResponse.FieldInfo field : fields) { + html.append(""); + } + } + + html.append(""); + html.append(""); + + // 表身 + html.append(""); + for (Map row : data) { + html.append(""); + for (AIReportResponse.FieldInfo field : fields) { + html.append(""); + } + html.append(""); + } + html.append(""); + + html.append("
"); + html.append(field.getComment()); + html.append("
"); + Object value = row.get(field.getName()); + html.append(value != null ? value.toString() : ""); + html.append("
"); + html.append("
"); + html.append("
"); + + return html.toString(); + } + + /** + * 生成复合表头 + */ + private String generateComplexTableHeader(List fields) { + StringBuilder html = new StringBuilder(); + + // 示例:销售数据复合表头 + html.append("物料信息"); + html.append("库存信息"); + html.append("供应商"); + + return html.toString(); + } + + /** + * 生成柱状图HTML模拟 + */ + private String generateBarChartHTML(List> data, List fields) { + StringBuilder html = new StringBuilder(); + html.append("
"); + + for (int i = 0; i < Math.min(data.size(), 8); i++) { + Map row = data.get(i); + int value = getNumericValue(row, fields); + int height = Math.max(value / 1000, 10); // 最小高度10px + + html.append("
"); + html.append("
"); + html.append("").append(value).append(""); + html.append("
"); + html.append("").append(getNameValue(row, fields)).append(""); + html.append("
"); + } + + html.append("
"); + return html.toString(); + } + + /** + * 生成折线图HTML模拟 + */ + private String generateLineChartHTML(List> data, List fields) { + StringBuilder html = new StringBuilder(); + html.append("
"); + html.append(""); + + int width = 600; + int height = 200; + int padding = 40; + + // 绘制坐标轴 + html.append(""); + html.append(""); + + // 绘制折线 + if (!data.isEmpty()) { + html.append(""); + + // 绘制数据点 + for (int i = 0; i < data.size(); i++) { + int x = padding + i * xStep; + int y = height - padding - (i * 20 % (height - 2 * padding)); + html.append(""); + } + } + + html.append(""); + html.append("
"); + return html.toString(); + } + + /** + * 生成饼图HTML模拟 + */ + private String generatePieChartHTML(List> data, List fields) { + StringBuilder html = new StringBuilder(); + html.append("
"); + html.append("
"); + html.append("
"); + return html.toString(); + } + + /** + * 生成分析建议 + */ + private String generateAnalysis(String prompt, List> data) { + if (data.isEmpty()) { + return "暂无数据可供分析。"; + } + + StringBuilder analysis = new StringBuilder(); + analysis.append("📊 **数据分析报告**\n\n"); + + if (prompt.toLowerCase().contains("top10") && prompt.toLowerCase().contains("库存")) { + analysis.append("**库存分析:**\n"); + analysis.append("- 库存数量排名前10的物料已列出\n"); + analysis.append("- 建议重点关注库存价值较高的物料\n"); + analysis.append("- 可考虑优化库存周转率较低的物料\n\n"); + } + + analysis.append("**关键指标:**\n"); + analysis.append("- 数据记录数:").append(data.size()).append("条\n"); + analysis.append("- 分析时间:").append(new Date().toLocaleString()).append("\n"); + analysis.append("- 数据完整性:良好\n\n"); + + analysis.append("**建议操作:**\n"); + analysis.append("- 可导出数据进一步分析\n"); + analysis.append("- 支持多维度筛选查看\n"); + analysis.append("- 建议定期更新数据保持分析时效性\n"); + + return analysis.toString(); + } + + /** + * 检查字段是否存在 + */ + private boolean hasField(List fields, String fieldName) { + return fields.stream().anyMatch(f -> f.getName().equals(fieldName)); + } + + /** + * 获取数值字段的值 + */ + private int getNumericValue(Map row, List fields) { + // 优先获取金额或数量字段 + String[] numericFields = {"sales_amount", "stock_value", "total_amount", "stock_quantity", "sales_quantity"}; + + for (String field : numericFields) { + Object value = row.get(field); + if (value instanceof Number) { + return ((Number) value).intValue(); + } + } + + // 如果没有找到数值字段,返回随机值 + return RANDOM.nextInt(1000) + 100; + } + + /** + * 获取名称字段的值 + */ + private String getNameValue(Map row, List fields) { + String[] nameFields = {"material_name", "product_name", "supplier_name"}; + + for (String field : nameFields) { + Object value = row.get(field); + if (value != null) { + return value.toString(); + } + } + + return "未知"; + } + + /** + * 从字符串中提取完整的HTML文档内容 + * + * @param input 输入字符串 + * @return 提取的HTML内容,如果未找到则返回空字符串 + */ + public static String extractHtmlContent(String input) { + if (input == null || input.isEmpty()) { + return ""; + } + + // 正则表达式匹配以开头,以结束的内容 + // Pattern.DOTALL 让 . 匹配包括换行符的所有字符 + String regex = "(?s).*?"; + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(input); + + if (matcher.find()) { + return matcher.group(0); + } + + return ""; + } + + + /** + * 创建标准化的错误事件JSON字符串 + */ + private String createErrorEvent(Throwable throwable) { + Map errorEvent = new LinkedHashMap<>(); + errorEvent.put("event", "error"); + errorEvent.put("status", "error"); + errorEvent.put("timestamp", System.currentTimeMillis()); + + // 提供详细的错误信息 + errorEvent.put("message", throwable.getMessage()); + errorEvent.put("errorType", throwable.getClass().getSimpleName()); + + // 开发环境下可以返回堆栈跟踪,生产环境应该关闭 +// if (isDevelopmentEnvironment()) { +// StringWriter sw = new StringWriter(); +// throwable.printStackTrace(new PrintWriter(sw)); +// errorEvent.put("stackTrace", sw.toString()); +// } + + try { + return objectMapper.writeValueAsString(errorEvent); + } catch (JsonProcessingException e) { + // 如果JSON序列化失败,返回最简错误格式 + return "{\"event\":\"error\",\"status\":\"error\",\"message\":\"系统内部错误\"}"; + } + } + +} diff --git a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageServiceImpl.java b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageServiceImpl.java index 17c25f7d..9b08d7c2 100644 --- a/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageServiceImpl.java +++ b/ruoyi-modules/hwmom-ai/src/main/java/org/dromara/ai/service/impl/AiChatMessageServiceImpl.java @@ -5,6 +5,7 @@ import com.github.yulichang.interfaces.MPJBaseJoin; import org.dromara.ai.domain.AiChatMessageDetail; import org.dromara.ai.domain.bo.AiChatMessageTopicBo; import org.dromara.ai.mapper.AiChatMessageDetailMapper; +import org.dromara.common.constant.HwMomAiConstants; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -182,4 +183,18 @@ public class AiChatMessageServiceImpl implements IAiChatMessageService { .eq(AiChatMessage::getSessionId, sessionId)) > 0; } + /** + * 查询符合条件的聊天消息列表(不查询生成报表的数据) + * + * @param bo 查询条件 + * @return 聊天消息列表 + */ + @Override + public List queryQaList(AiChatMessageBo bo) { + MPJLambdaWrapper lqw = buildQueryWrapper(bo); + lqw.ne(AiChatMessage::getMessageType, HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT); + + return baseMapper.selectVoList(lqw); + } + } diff --git a/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/SQLServerDatabaseMetaMapper.xml b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/SQLServerDatabaseMetaMapper.xml index d6d2e51b..ecf7fb6c 100644 --- a/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/SQLServerDatabaseMetaMapper.xml +++ b/ruoyi-modules/hwmom-ai/src/main/resources/mapper/ai/SQLServerDatabaseMetaMapper.xml @@ -145,4 +145,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + +