From d72de0c4fe4d50edf536174f6b7f6fa97d18ac96 Mon Sep 17 00:00:00 2001 From: qianbao Date: Mon, 30 Mar 2026 14:14:03 +0800 Subject: [PATCH] 添加向量库 --- pom.xml | 6 ++++++ src/main/java/com/xly/agent/ErpAiAgent.java | 332 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------- src/main/java/com/xly/milvus/service/MilvusService.java | 8 ++++---- src/main/java/com/xly/milvus/service/impl/AiGlobalAgentQuestionSqlEmitterServiceImpl.java | 12 +++++++++++- src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------- src/main/java/com/xly/service/XlyErpService.java | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------ src/main/java/com/xly/thread/AiUserAgentQuestionThread.java | 14 ++++++-------- src/main/java/com/xly/util/SqlWhereHelper.java | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 581 insertions(+), 206 deletions(-) create mode 100644 src/main/java/com/xly/util/SqlWhereHelper.java diff --git a/pom.xml b/pom.xml index e693acd..01923e8 100644 --- a/pom.xml +++ b/pom.xml @@ -105,6 +105,12 @@ org.springframework.boot spring-boot-starter-webflux + + + com.github.jsqlparser + jsqlparser + 4.9 + diff --git a/src/main/java/com/xly/agent/ErpAiAgent.java b/src/main/java/com/xly/agent/ErpAiAgent.java index ba4f7d8..811d675 100644 --- a/src/main/java/com/xly/agent/ErpAiAgent.java +++ b/src/main/java/com/xly/agent/ErpAiAgent.java @@ -93,24 +93,18 @@ public interface ErpAiAgent { @SystemMessage(""" 你是一个智能查询路由专家。请根据【用户需求】,只返回 true 或 false。 - 【最高优先级规则 - 必须首先判断】 如果用户需求包含以下任一关键词,**直接返回 false**,不再进行其他判断: - - 明细、详情、详细信息、详细内容、具体内容 - - 查询...明细、...详情、...记录、...列表、...清单 - + - 明细、列表、清单 + - ...明细、...列表、...清单 重要:只要出现以上关键词,说明用户需要的是明细数据查询,而非统计分析。 - 【统计类关键词 - 仅在满足最高优先级规则后才判断】 只有当用户需求不包含上述明细类关键词时,才检查是否包含以下关键词: 统计、求和、汇总、排名、TopN、平均、数量、总额、最高、最低、占比、分组 - - 如果包含,返回 true - 否则返回 false - 【判断示例】 - \"查询中科精工集团的彩盒类产品的报价单明细\" → false(包含\"明细\") - - \"统计各产品销售额\" → true(包含\"统计\",且无明细关键词) - \"查询客户张三信息\" → false(无统计关键词,无明细关键词) - \"销售额排名前10的产品\" → true(包含\"排名\",且无明细关键词) - \"查看销售订单明细\" → false(包含\"明细\") @@ -119,90 +113,264 @@ public interface ErpAiAgent { 【用户需求】 {{userInput}} """) +// @SystemMessage(""" +// 你是一个智能查询路由专家,请根据【用户需求】,基于**查询效率最优**原则,自动判断使用关系型数据库(MySQL)还是向量库(Milvus),只返回 true 或 false。 +// 满足如下规则中任意一条则返回true 否则返回false: +// - 查询涉及**排名、TOP N、求和、计数、平均值、最大值、总额、最小值、最高、最低、占比** +// - 查询涉及**分组统计(GROUP BY)、排序(ORDER BY)、分页(LIMIT)** +// """) +// @UserMessage(""" +// 【用户需求】 +// {{userInput}} +// """) Boolean routeQuery(@MemoryId String userId, @V("userInput") String userInput); /** * 生成 Milvus 过滤条件(适配 Milvus v2.3.9) */ @SystemMessage(""" - MILVUS 标量过滤条件生成规则(严格遵守 - 当前版本 v2.3.9): - - 【重要输出约束】 - - 必须返回有效的 Milvus 过滤条件表达式 - - 禁止返回 true 或 false - - 禁止返回空字符串以外的任何非表达式内容 - - 无条件时只返回空字符串 "" - - 1. 语法规范: - - 允许的操作符:==, !=, like - - 逻辑组合:&& (AND), || (OR) - - 所有字段都是字符串类型,值必须使用单引号包裹 - - 字符串中的单引号需要转义:'O''Reilly' - - 2. 【重要】Milvus v2.3.9 like 操作符限制: - - ✅ 支持:like '关键字%'(前缀匹配,以关键字开头) - - ❌ 不支持:like '%关键字%'(包含匹配) - - ❌ 不支持:like '%关键字'(后缀匹配) - - 3. 可用字段(只能使用这些字段): - - {{sMilvusFiled}} - 字段说明: - - {{sMilvusFiledDescription}} - - 4. 提取规则: - - 只使用上述可用字段,不要创建新字段 - - 如果用户提到了文档类型(如"报价单"、"订单"等),但可用字段中没有类型字段,则忽略该条件 - - 【精确匹配规则】: - - 当用户提供明确值时:字段 == '值' - * 例如:"客户名称中科精工" → sCustomerName == '中科精工' - * 例如:"单据号 INV001" → sBillNo == 'INV001' - - 5. 时间处理规则: - - 当前系统时间:{{sDataNow}}(格式:yyyy-MM-dd) - - 相对时间转换规则: - * "今天/今日" → 当天 00:00:00 到 23:59:59 - * "昨天" → 前一天 00:00:00 到 23:59:59 - * "本周" → 本周一 00:00:00 到本周日 23:59:59 - * "本月" → 本月1日 00:00:00 到本月最后一天 23:59:59 - * "本年" → 本年1月1日 00:00:00 到本年12月31日 23:59:59 - * "近X天" → 从 X 天前 00:00:00 到今天 23:59:59 - - 日期转时间戳:所有日期转换为 Unix 时间戳(秒) - - 时间范围格式:字段 >= 起始时间戳 && 字段 <= 结束时间戳 - - 如果没有明确的时间需求,不要添加任何时间过滤条件 - - 6. 示例: - ✅ 正确输出: - - "客户名称中科精工" → sCustomerName == '中科精工' - - "中科精工的报价单明细" → sCustomerName == '中科精工' - - "产品以彩盒开头" → sProductStyle like '彩盒%' - - "无条件" → "" - - ❌ 错误输出(禁止): - - "中科精工的报价单明细" → true - - "中科精工的报价单明细" → false - - "中科精工的报价单明细" → 1 - - 7. 输出格式: - - 仅返回纯过滤条件,无任何解释、换行、备注 - - 单条件:sCustomerName == '中科精工' - - 多条件:(sCustomerName == '中科精工' && sProductStyle like '彩盒%') - - 无条件:直接返回空字符串 "" + MILVUS 查询条件生成规则: + + 【最高优先级 - 输出格式铁律】 + ⚠️ 你的【全部输出】必须是且仅是一个合法的 JSON 对象 + ⚠️ 禁止输出任何解释、说明、思考过程 + ⚠️ 禁止输出任何中文文字 + ⚠️ 只能输出以下 JSON 格式,不能有其他任何内容 + + 【输出 JSON 结构】 + { + "sMethodName": true/false, // 必选,判断用户意图是否匹配当前方法 + "vectorField": "向量字段名", // 可选,需要语义匹配时返回 + "vectorValue": "向量化文本", // 可选,用于向量检索的文本 + "filterExpression": "标量过滤表达式" // 可选,有标量条件时返回 + } + + 【方法匹配规则 - 重要】 + sMethodName 的取值逻辑: + - 如果用户输入的意图与 {{sMethodName}} 相关,返回 true + - 否则返回 false + + 判断标准:用户是否在询问或操作与 {{sMethodName}} 相关的业务数据 + - 包括但不限于:查询、搜索、推荐、找、查看、明细、列表、详情等 + - 只要用户想获取或操作这类数据,就应该返回 true + + 示例: + - 方法名称:查询报价单 + - "报价单明细" → true(想查看报价单数据) + - "报价单列表" → true(想查看报价单数据) + - "报价单" → true(想查看报价单数据) + - "查一下报价单" → true(想查询报价单) + - "推荐相似报价单" → true(想推荐报价单) + - "你好" → false(与报价单无关) + - "查询客户信息" → false(与报价单无关) + + 【重要:理解你的数据结构】 + + 你有两种类型的字段: + + 1. 标量字段(用于精确过滤): + - {{sMilvusFiled}} 中的字段 + - 根据提供的字段说明使用 + + 2. 向量字段(用于语义搜索): + - 字段名:使用 {{sMilvusFiledXl}} 中提供的向量字段名 + - 存储格式:管道符分隔的键值对 "字段名:值|字段名:值|..." + - 包含的业务数据:{{sMilvusFiledDescriptionXl}} + - 示例:"sCustomerName:上海小羚羊|sProductName:2028宣传海报|dProductQty:1000|sType:报价丢单原因" + + 【向量搜索规则】 + + 使用向量搜索的条件:用户明确表达语义匹配意图 + - 关键词:找相似、推荐、匹配、类似、相关、类似的、相似的、推荐一下 + - 使用向量搜索时: + 1. 从用户问题中提取关键业务实体 + 2. 格式化为:"字段名:值|字段名:值"(管道符分隔) + 3. 示例: + - 用户:"找丢单原因类似价格太高的记录" → "sReason:价格太高|sType:报价丢单原因" + - 用户:"推荐和上海小羚羊类似的客户" → "sCustomerName:上海小羚羊" + - 用户:"找类似2028宣传海报的产品" → "sProductName:2028宣传海报" + + 【标量过滤规则 - 极其重要】 + + 可用标量字段(根据 {{sMilvusFiled}} 动态提供): + - 使用提供的字段名 + - 【字符串字段】:统一使用 like 操作符(前缀匹配) + - 【数字字段】:使用 >、<、>=、<=、== 操作符 + - 【时间字段】:使用时间戳范围 + + 【操作符使用规则】 + + 1. 字符串字段(统一使用 like): + - 语法:字段名 like '关键词%' + - 说明:只支持前缀匹配,不支持 '%keyword%' 或 '%keyword' + - 示例: + - 用户:"上海小羚羊" → sCustomerName like '上海小羚羊%' + - 用户:"客户名称包含上海" → sCustomerName like '上海%' + - 用户:"姓张的销售人员" → sSalesManName like '张%' + - 用户:"产品名称以2028开头" → sProductName like '2028%' + - 用户:"张三" → sSalesManName like '张三%' + + 2. 数字字段: + - 精确匹配:字段名 == 值 + - 范围匹配:字段名 >= 值 && 字段名 <= 值 + - 大于:字段名 > 值 + - 小于:字段名 < 值 + - 示例: + - 用户:"数量1000" → dProductQty == 1000 + - 用户:"数量大于1000" → dProductQty > 1000 + - 用户:"数量在500到1000之间" → dProductQty >= 500 && dProductQty <= 1000 + + 3. 时间字段: + - 使用时间戳范围:tCreateDate >= 开始时间戳 && tCreateDate <= 结束时间戳 + - 时间戳为 Unix 秒 + + 4. 组合条件: + - 使用 && (AND) 连接多个条件 + - 使用 || (OR) 连接或条件 + - 复杂条件用括号分组 + + 【核心约束 - 必须遵守】 + 1. 只有当用户【明确指定】具体条件时,才生成 filterExpression + 2. 禁止为模糊查询添加默认过滤条件(如时间范围) + 3. 禁止自动添加任何默认条件 + 4. 当前时间戳:{{sDataNow}}(Unix秒) + 5. 字符串字段禁止使用 ==,必须使用 like + 6. like 只支持前缀匹配,不支持 '%keyword%' 或 '%keyword' 格式 + + 【查询意图判断】 + + 类型A - 纯标量查询(只返回 filterExpression): + - 用户明确指定了标量字段的具体条件 + - 不包含语义匹配词 + - 示例1:"上海小羚羊" → {"sMethodName": true, "filterExpression": "sCustomerName like '上海小羚羊%'"} + - 示例2:"客户名称包含上海" → {"sMethodName": true, "filterExpression": "sCustomerName like '上海%'"} + - 示例3:"张三" → {"sMethodName": true, "filterExpression": "sSalesManName like '张三%'"} + - 示例4:"数量大于1000" → {"sMethodName": true, "filterExpression": "dProductQty > 1000"} + - 示例5:"今天创建的报价单" → {"sMethodName": true, "filterExpression": "tCreateDate >= 开始时间戳 && tCreateDate <= 结束时间戳"} + + 类型B - 纯向量搜索(只返回 vectorField 和 vectorValue): + - 用户只有语义意图,无具体标量条件 + - 示例:"找一些类似的报价单" → {"sMethodName": true, "vectorField": "向量字段名", "vectorValue": "sType:报价丢单原因"} + + 类型C - 混合查询(同时返回 vectorField、vectorValue 和 filterExpression): + - 用户既有具体条件,又需要语义匹配 + - 示例:"找丢单原因类似价格太高的上海小羚羊报价单" → + {"sMethodName": true, "vectorField": "向量字段名", "vectorValue": "sReason:价格太高|sType:报价丢单原因", "filterExpression": "sCustomerName like '上海小羚羊%'"} + + 类型D - 意图不匹配(只返回 sMethodName: false): + - 用户输入与 {{sMethodName}} 完全无关 + - 示例:"你好" → {"sMethodName": false} + - 示例:"查询客户信息"(如果方法名是"查询报价单") → {"sMethodName": false} + + 类型E - 意图匹配但无具体条件(只返回 sMethodName: true): + - 用户想查询该类数据,但没有指定任何条件 + - 示例:"报价单明细" → {"sMethodName": true} + - 示例:"报价单列表" → {"sMethodName": true} + - 示例:"报价单" → {"sMethodName": true} + + 【时间处理规则】 + + 当用户明确提到时间时,基于当前时间 {{sDataNow}} 计算: + - "今天":当天 00:00:00 到 23:59:59 + - "昨天":前一天 00:00:00 到 23:59:59 + - "本周":本周一 00:00:00 到本周日 23:59:59 + - "本月":本月1日 00:00:00 到本月最后一天 23:59:59 + - "上个月":上个月1日 00:00:00 到上个月最后一天 23:59:59 + - "近X天":X天前 00:00:00 到今天 23:59:59 + + 时间范围表达式格式:tCreateDate >= 开始时间戳 && tCreateDate <= 结束时间戳 + + 【重要约束汇总】 + 1. 只输出 JSON,不要有任何其他内容 + 2. 只使用提供的标量字段 + 3. 【关键】字符串字段必须使用 like,禁止使用 == + 4. 时间范围必须使用 Unix 时间戳(秒) + 5. 向量字段名使用 {{sMilvusFiledXl}} 中提供的字段名 + 6. vectorValue 必须保持管道符分隔格式:"字段名:值|字段名:值" + 7. 禁止为模糊查询添加默认条件 + 8. sMethodName 只要用户意图与当前方法相关就返回 true + 9. like 只支持前缀匹配,格式:字段名 like '值%' + + 【输出示例】 + 方法名称:查询报价单 + + 示例1:模糊查询 - "报价单明细" + 输出:{"sMethodName": true} + + 示例2:字符串模糊匹配 - "上海小羚羊" + 输出:{"sMethodName": true, "filterExpression": "sCustomerName like '上海小羚羊%'"} + + 示例3:字符串模糊匹配 - "客户名称包含上海" + 输出:{"sMethodName": true, "filterExpression": "sCustomerName like '上海%'"} + + 示例4:字符串模糊匹配 - "张三" + 输出:{"sMethodName": true, "filterExpression": "sSalesManName like '张三%'"} + + 示例5:数字范围 - "数量大于1000" + 输出:{"sMethodName": true, "filterExpression": "dProductQty > 1000"} + + 示例6:时间范围 - "今天创建的报价单" + 输出:{"sMethodName": true, "filterExpression": "tCreateDate >= 1774754400 && tCreateDate <= 1774840799"} + + 示例7:组合条件 - "上海小羚羊且数量大于1000" + 输出:{"sMethodName": true, "filterExpression": "sCustomerName like '上海小羚羊%' && dProductQty > 1000"} + + 示例8:纯向量 - "找一些类似的报价单" + 输出:{"sMethodName": true, "vectorField": "content_embedding", "vectorValue": "sType:报价丢单原因"} + + 示例9:混合查询 - "找丢单原因类似价格太高的上海小羚羊报价单" + 输出:{"sMethodName": true, "vectorField": "content_embedding", "vectorValue": "sReason:价格太高|sType:报价丢单原因", "filterExpression": "sCustomerName like '上海小羚羊%'"} + + 示例10:意图不匹配 - "你好" + 输出:{"sMethodName": false} + + 【标量字段列表】 + {{sMilvusFiled}} + + 【标量字段说明】 + {{sMilvusFiledDescription}} + + 【向量字段列表】 + {{sMilvusFiledXl}} + + 【向量字段说明】 + {{sMilvusFiledDescriptionXl}} + + 【方法名称】 + {{sMethodName}} + """) - @UserMessage(""" - 【用户查询】 - - {{userInput}} + @UserMessage(""" + 【用户输入】 + {{userInput}} + 【当前时间】 - - {{sDataNow}} - 【可用字段】 - - {{sMilvusFiled}} - 【字段说明】 - - {{sMilvusFiledDescription}} - """) + {{sDataNow}} + + 【标量字段】 + {{sMilvusFiled}} + + 【标量字段说明】 + {{sMilvusFiledDescription}} + + 【向量字段】 + {{sMilvusFiledXl}} + + 【向量字段说明】 + {{sMilvusFiledDescriptionXl}} + + 【方法名称】 + {{sMethodName}} + + 请根据以上规则,输出 JSON 格式结果。 +""") String getMilvusFilter(@MemoryId String userId, @V("userInput") String userInput, @V("sMilvusFiled") String sMilvusFiled, @V("sMilvusFiledDescription") String sMilvusFiledDescription, - @V("sDataNow") String sDataNow); - + @V("sMilvusFiledXl") String sMilvusFiledXl, + @V("sMilvusFiledDescriptionXl") String sMilvusFiledDescriptionXl, + @V("sDataNow") long sDataNow, + @V("sMethodName") String sMethodName + ); } diff --git a/src/main/java/com/xly/milvus/service/MilvusService.java b/src/main/java/com/xly/milvus/service/MilvusService.java index 7960561..1ecee9e 100644 --- a/src/main/java/com/xly/milvus/service/MilvusService.java +++ b/src/main/java/com/xly/milvus/service/MilvusService.java @@ -36,7 +36,7 @@ public interface MilvusService { * @return long * @Description 批量插入数据 **/ - long addDataToCollection(String collectionName, String sVectorfiled, String sVectorjson, List> data); + long addDataToCollection(String collectionName, String sVectorfiled, String sVectorjson, List> data,String sceneName); /*** * @Author 钱豹 @@ -45,7 +45,7 @@ public interface MilvusService { * @return * @Description 向量库查询 **/ - List> getDataToCollection(String collectionName, String milvusFilter,String searchText,Integer size,List fields); + List> getDataToCollection(String collectionName, String milvusFilter,String searchText,Integer size,List fields,String vectorValue,String sceneName); /*** * @Author 钱豹 @@ -54,7 +54,7 @@ public interface MilvusService { * @return java.util.Map * @Description 获取配置 **/ - Map getMilvusFiled(String sVectorfiled,String sVectorfiledAll,String sVectorfiledShow); + Map getMilvusFiled(String sVectorfiled,String sVectorfiledAll,String sVectorfiledShow,String sVectorjson); - boolean isValidMilvusFilter(String milvusFilter); + boolean isStringFilterValid(String filter,String collectionName); } \ No newline at end of file diff --git a/src/main/java/com/xly/milvus/service/impl/AiGlobalAgentQuestionSqlEmitterServiceImpl.java b/src/main/java/com/xly/milvus/service/impl/AiGlobalAgentQuestionSqlEmitterServiceImpl.java index d178dcf..0e5610f 100644 --- a/src/main/java/com/xly/milvus/service/impl/AiGlobalAgentQuestionSqlEmitterServiceImpl.java +++ b/src/main/java/com/xly/milvus/service/impl/AiGlobalAgentQuestionSqlEmitterServiceImpl.java @@ -8,6 +8,7 @@ import com.google.gson.JsonObject; import com.xly.milvus.service.AiGlobalAgentQuestionSqlEmitterService; import com.xly.milvus.service.VectorizationService; import com.xly.milvus.util.MapToJsonConverter; +import com.xly.service.DynamicExeDbService; import io.milvus.v2.client.MilvusClientV2; import io.milvus.v2.common.ConsistencyLevel; import io.milvus.v2.common.DataType; @@ -37,11 +38,15 @@ public class AiGlobalAgentQuestionSqlEmitterServiceImpl implements AiGlobalAgent private final MilvusClientV2 milvusClient; private final VectorizationService vectorizationService; + private final DynamicExeDbService dynamicExeDbService; + private final String sProName ="Sp_Ai_AiGlobalAgentQuestionThread"; + // 或者从配置文件读取 @Value("${milvus.vector.dimension:384}") private int VECTOR_DIM; + // 缓存已加载的集合 private final Set loadedCollections = new ConcurrentHashSet<>(); @@ -78,6 +83,11 @@ public class AiGlobalAgentQuestionSqlEmitterServiceImpl implements AiGlobalAgent .build(); InsertResp insertResp = milvusClient.insert(insertReq); + if(insertResp.getInsertCnt()>0){ + //调用数据库插入数据库 + Map searMap = dynamicExeDbService.getDoProMap(sProName, data); + dynamicExeDbService.getCallPro(searMap, sProName); + } System.out.println("成功插入 " + insertResp.getInsertCnt() + " 条数据"); System.out.println(" - 数据预览:"); } @@ -121,7 +131,7 @@ public class AiGlobalAgentQuestionSqlEmitterServiceImpl implements AiGlobalAgent .annsField("vector") // 向量字段名 .topK(10) // 返回最相似的10条 .metricType(IndexParam.MetricType.IP) // 内积相似度 - .outputFields(Arrays.asList("sQuestion", "sSqlContent", "data_id","db_name", "create_time","metadata")) + .outputFields(Arrays.asList("sQuestion", "sSqlContent", "data_id","cachType", "create_time","metadata")) .searchParams(searchParams) .build(); // 5. 执行搜索 diff --git a/src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java b/src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java index 8c4ebcb..4baa380 100644 --- a/src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java +++ b/src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java @@ -16,6 +16,10 @@ import com.xly.milvus.util.MapToJsonConverter; import com.xly.milvus.util.MilvusTimeUtil; import com.xly.service.DynamicExeDbService; import com.xly.tts.bean.TTSResponseDTO; +import io.milvus.client.MilvusServiceClient; +import io.milvus.grpc.QueryResults; +import io.milvus.param.R; +import io.milvus.param.dml.QueryParam; import io.milvus.v2.client.MilvusClientV2; import io.milvus.v2.common.ConsistencyLevel; import io.milvus.v2.common.DataType; @@ -23,9 +27,11 @@ import io.milvus.v2.common.IndexParam; import io.milvus.v2.service.collection.request.*; import io.milvus.v2.service.vector.request.DeleteReq; import io.milvus.v2.service.vector.request.InsertReq; +import io.milvus.v2.service.vector.request.QueryReq; import io.milvus.v2.service.vector.request.SearchReq; import io.milvus.v2.service.vector.request.data.FloatVec; import io.milvus.v2.service.vector.response.InsertResp; +import io.milvus.v2.service.vector.response.QueryResp; import io.milvus.v2.service.vector.response.SearchResp; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -94,10 +100,17 @@ public class MilvusServiceImpl implements MilvusService { .message("向量化内容JSON") .build(); } + if(ObjectUtil.isEmpty(reqMap.get("sceneName"))){ + return TTSResponseDTO.builder() + .code(-1) + .message("未传入场景名称") + .build(); + } String sInputTabelName = reqMap.get("sInputTabelName").toString(); String sVectorfiled = reqMap.get("sVectorfiled").toString(); String sVectorjson = reqMap.get("sVectorjson").toString(); + String sceneName = reqMap.get("sceneName").toString(); //创建集合 createCollectionIfNotExists(sInputTabelName, sVectorfiled, sVectorjson,true); String tUpdateDate = DateUtil.now(); @@ -106,7 +119,7 @@ public class MilvusServiceImpl implements MilvusService { List> data = getAddData(sInputTabelName,tUpdateDate, tUpdateDateUp); if(ObjectUtil.isNotEmpty(data)){ //插入数据 - long num= addDataToCollection(sInputTabelName, sVectorfiled, sVectorjson,data); + long num= addDataToCollection(sInputTabelName, sVectorfiled, sVectorjson,data,sceneName); } addAiMilvusVectorRecord(sInputTabelName,tUpdateDate, tUpdateDateUp); return TTSResponseDTO.builder() @@ -202,7 +215,7 @@ public class MilvusServiceImpl implements MilvusService { * @Description 新增数据集合 **/ @Override - public long addDataToCollection(String collectionName, String sVectorfiled, String sVectorjson,List> data){ + public long addDataToCollection(String collectionName, String sVectorfiled, String sVectorjson,List> data,String sceneName){ // 1. 参数校验(防止空参数导致崩溃) if (ObjectUtil.isEmpty(collectionName) || CollUtil.isEmpty(data)) { @@ -210,7 +223,7 @@ public class MilvusServiceImpl implements MilvusService { } // 1. 转换为Milvus格式 - List rows = convertToMilvusRow(data, sVectorfiled, sVectorjson); + List rows = convertToMilvusRow(data, sVectorfiled, sVectorjson,sceneName); if (CollUtil.isEmpty(rows)) { return 0l; // 无数据直接返回 } @@ -264,7 +277,7 @@ public class MilvusServiceImpl implements MilvusService { * @Description 返回组装动态内容 **/ @Override - public Map getMilvusFiled(String sVectorfiled,String sVectorfiledAll,String sVectorfiledShow){ + public Map getMilvusFiled(String sVectorfiled,String sVectorfiledAll,String sVectorfiledShow,String sVectorjson){ List sFileds = new ArrayList<>(); List filedsShow = new ArrayList<>(); List sFiledDescriptions = new ArrayList<>(); @@ -299,17 +312,32 @@ public class MilvusServiceImpl implements MilvusService { String formattedDesc =String.format("%s: %s", sName, sDescriptions); sFiledDescriptionsAll.add(formattedDesc); } + List sFiledsXl = new ArrayList<>(); + List sFiledDescriptionsXl = new ArrayList<>(); + String[] sVectorjsonArray = sVectorjson.split(","); + for(String sVectorjsonOne : sVectorjsonArray){ + String[] sVectorfiledOneArray = sVectorjsonOne.split(":"); + String sDescriptions = sVectorfiledOneArray[0]; + String sName = sVectorfiledOneArray[1]; + sFiledsXl.add(sName); + // 处理描述中可能包含的换行,保持缩进一致 + String formattedDesc =String.format("%s: %s", sName, sDescriptions); + sFiledDescriptionsXl.add(formattedDesc); + } + Map rMap = new HashMap<>(); rMap.put("sMilvusFiled", String.join(",", sFileds)); rMap.put("sMilvusFiledDescription", String.join(",", sFiledDescriptions)); rMap.put("sMilvusFiledDescriptionAll", String.join(",", sFiledDescriptionsAll)); + rMap.put("sMilvusFiledXl", String.join(",", sFiledsXl)); + rMap.put("sMilvusFiledDescriptionXl", String.join(",", sFiledDescriptionsXl)); rMap.put("filedsShow", filedsShow); rMap.put("title", titleList); return rMap; } @Override - public List> getDataToCollection(String collectionName, String milvusFilter,String searchText,Integer size,List fields){ + public List> getDataToCollection(String collectionName, String milvusFilter,String searchText,Integer size,List fields,String vectorValue,String sceneName){ log.info("开始相似度查询: collection={}, searchText={}", collectionName, searchText); // 2. 设置范围搜索参数 Map searchParams = new HashMap<>(); @@ -317,10 +345,24 @@ public class MilvusServiceImpl implements MilvusService { // 对于 IP 度量,相似度范围在 [minScore, maxScore] searchParams.put("radius", 0.9); // 最小相似度 searchParams.put("range_filter", 1); // 最大相似度 - // 1. 确保集合已加载 -// ensureCollectionLoaded(collectionName); + if(ObjectUtil.isEmpty(fields)){ + fields = new ArrayList<>(); + } + fields.add("sSlaveId"); + fields.add("metadata"); + // 1. 构建查询(通用) + SearchReq.SearchReqBuilder builder = SearchReq.builder() + .collectionName(collectionName) + .topK(size) + .metricType(IndexParam.MetricType.IP) + .outputFields(fields) +// .filterType(FilterType.POST_FILTER) + .searchParams(searchParams); + if(ObjectUtil.isEmpty(vectorValue)){ + vectorValue = sceneName; + } // 1. 向量化搜索文本 - List vectorList = vectorizationService.textToVector(searchText); + List vectorList = vectorizationService.textToVector(vectorValue); if (vectorList == null || vectorList.isEmpty()) { throw new RuntimeException("向量化失败"); } @@ -329,25 +371,27 @@ public class MilvusServiceImpl implements MilvusService { for (int i = 0; i < vectorList.size(); i++) { floatArray[i] = vectorList.get(i); } - if(ObjectUtil.isEmpty(fields)){ - fields = new ArrayList<>(); - } - fields.add("sSlaveId"); - fields.add("metadata"); // 3. 创建 Milvus FloatVec 对象 FloatVec floatVec = new FloatVec(floatArray); + builder.data(Collections.singletonList(floatVec)) + .annsField("vector"); // 向量字段名 + + if(ObjectUtil.isNotEmpty(milvusFilter)){ + builder.filter(milvusFilter); + } // 4. 构建搜索请求 - SearchReq searchReq = SearchReq.builder() - .collectionName(collectionName) - .data(Collections.singletonList(floatVec)) - .annsField("vector") // 向量字段名 - .topK(size) // 返回最相似的10条 - .metricType(IndexParam.MetricType.IP) // 内积相似度 - .outputFields(fields) +// SearchReq searchReq = SearchReq.builder() +// .collectionName(collectionName) +// .data(Collections.singletonList(floatVec)) +// .annsField("vector") // 向量字段名 +// .topK(size) // 返回最相似的10条 +// .metricType(IndexParam.MetricType.IP) // 内积相似度 +// .outputFields(fields) // .searchParams(searchParams) - .filter(milvusFilter) - .build(); +// .filter(milvusFilter) +// .build(); // 5. 执行搜索 + SearchReq searchReq = builder.build(); SearchResp searchResp = milvusClient.search(searchReq); // 6. 处理结果 @@ -357,45 +401,26 @@ public class MilvusServiceImpl implements MilvusService { /** * 判断 Milvus 过滤条件是否有效(支持 TEXT_MATCH 全文检索) - * @param milvusFilter 过滤条件字符串 * @return true: 有效条件, false: 无效条件 */ - public boolean isValidMilvusFilter(String milvusFilter) { - // 1. 空值判断 - if (milvusFilter == null || milvusFilter.trim().isEmpty()) { - return false; - } - - String filter = milvusFilter.trim(); - - // 2. 基本格式检查:不能是纯布尔值 - if ("true".equalsIgnoreCase(filter) || "false".equalsIgnoreCase(filter)) { - return false; - } - - // 3. 【修改】检查是否包含有效的操作符(增加 TEXT_MATCH 支持) - boolean hasValidOperator = filter.matches(".*[=!<>]=?.*") - || filter.contains(" like ") - || filter.toUpperCase().contains("TEXT_MATCH"); - - if (!hasValidOperator) { - return false; - } - - // 4. 对于复合条件,递归检查 - if (filter.contains("&&") || filter.contains("||")) { - // 分割复合条件(简单处理,生产环境需要更完善的解析) - String[] conditions = splitConditions(filter); - for (String condition : conditions) { - if (!isValidCondition(condition)) { - return false; - } - } + /** + * 判断字符串 Filter 是否有效 + * @param filter 过滤表达式,如 "name == '张三'" + * @return true=有效,false=无效 + */ + public boolean isStringFilterValid(String filter,String collectionName) { + try { + // 使用 limit 为 0 只验证语法,不实际返回数据 + QueryReq queryReq = QueryReq.builder() + .collectionName(collectionName) + .filter(filter) // 设置过滤条件 + .limit(0) // limit 0 只校验语法 + .build(); + QueryResp response = milvusClient.query(queryReq); return true; + } catch (Exception e) { + return false; } - - // 5. 检查单个条件 - return isValidCondition(filter); } /** @@ -583,14 +608,14 @@ public class MilvusServiceImpl implements MilvusService { /** * 从实体对象构建Milvus插入数据 */ - public List convertToMilvusRow(List> data, String sVectorfiled,String sVectorjson) { + public List convertToMilvusRow(List> data, String sVectorfiled,String sVectorjson,String sceneName) { List rows = new ArrayList<>(); if (CollUtil.isEmpty(data)) { return rows; } // 批量遍历,逐个转换 for (Map map : data) { - JsonObject jsonObject = convertToMilvusRowOne(map, sVectorfiled, sVectorjson); + JsonObject jsonObject = convertToMilvusRowOne(map, sVectorfiled, sVectorjson,sceneName); rows.add(jsonObject); } return rows; @@ -603,12 +628,12 @@ public class MilvusServiceImpl implements MilvusService { * @return com.google.gson.JsonObject * @Description 单个转换 **/ - public JsonObject convertToMilvusRowOne(Map data, String sVectorfiled,String sVectorjson) { + public JsonObject convertToMilvusRowOne(Map data, String sVectorfiled,String sVectorjson,String sceneName) { // ====================== 修复 1:使用真实的向量化文本 ====================== // 从 sVectorjson 或 data 中获取要向量化的字段值 StringBuffer vectorText = new StringBuffer(); - getVectorText(data, vectorText, sVectorjson); + getVectorText(data, vectorText, sVectorjson,sceneName); // 向量化 List vector = vectorizationService.textToVector(vectorText.toString()); if (vector == null || vector.isEmpty()) { @@ -650,7 +675,7 @@ public class MilvusServiceImpl implements MilvusService { return row; } - private void getVectorText(Map data, StringBuffer vectorText,String sVectorjson){ + private void getVectorText(Map data, StringBuffer vectorText,String sVectorjson,String sceneName){ // 动态字段 String[] sVectorjsonArray = sVectorjson.split(";"); for (String sVectorjsonOne : sVectorjsonArray) { @@ -666,8 +691,9 @@ public class MilvusServiceImpl implements MilvusService { }else{ sText = value.toString(); } - vectorText.append(" ").append(fieldArr[0]).append(sText); + vectorText.append(" | ").append(fieldArr[0]).append(":").append(sText); } + vectorText.append(" ").append(sceneName); } diff --git a/src/main/java/com/xly/service/XlyErpService.java b/src/main/java/com/xly/service/XlyErpService.java index 3631231..854ce0b 100644 --- a/src/main/java/com/xly/service/XlyErpService.java +++ b/src/main/java/com/xly/service/XlyErpService.java @@ -1,11 +1,11 @@ package com.xly.service; import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; -import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.xly.agent.ChatiAgent; @@ -405,25 +405,48 @@ public class XlyErpService { String sInputTabelName = session.getCurrentTool().getSInputTabelName(); String sVectorfiledAll = session.getCurrentTool().getSVectorfiledAll(); String sVectorfiledShow = session.getCurrentTool().getSVectorfiledShow(); - Map rMap = milvusService.getMilvusFiled(sVectorfiled,sVectorfiledAll,sVectorfiledShow); + String sVectorjson = session.getCurrentTool().getSVectorjson(); + String sceneName = session.getCurrentTool().getSceneName(); + String sMethodName = session.getCurrentTool().getSMethodName(); + Map rMap = milvusService.getMilvusFiled(sVectorfiled,sVectorfiledAll,sVectorfiledShow,sVectorjson); String sMilvusFiled = rMap.get("sMilvusFiled").toString(); + String sMilvusFiledXl = rMap.get("sMilvusFiledXl").toString(); String sMilvusFiledDescription = rMap.get("sMilvusFiledDescription").toString(); + String sMilvusFiledDescriptionXl = rMap.get("sMilvusFiledDescriptionXl").toString(); String sMilvusFiledDescriptionAll = rMap.get("sMilvusFiledDescriptionAll").toString(); List filedsShow = (List) rMap.get("filedsShow"); List> title = (List>) rMap.get("title"); String milvusFilter = StrUtil.EMPTY; + String vectorValue = StrUtil.EMPTY; + Boolean bMethodName = false; if(!bCach){ - milvusFilter = aiAgent.getMilvusFilter(session.getUserId(),userInput, sMilvusFiled, sMilvusFiledDescription,DateUtil.now()); - log.info("查询向量库条件{}",milvusFilter); - milvusFilter = milvusService.isValidMilvusFilter(milvusFilter)?milvusFilter : null; - log.info("实际查询向量库条件{}",milvusFilter); + long sDateNow = System.currentTimeMillis() / 1000; + String milvusFilterOld = aiAgent.getMilvusFilter(session.getUserId(),userInput, sMilvusFiled, sMilvusFiledDescription,sMilvusFiledXl, sMilvusFiledDescriptionXl,sDateNow,sMethodName); + log.info("查询向量库条件{}",milvusFilterOld); + if(ObjectUtil.isNotEmpty(milvusFilterOld) && JSONUtil.isTypeJSON(milvusFilterOld)){ + Map filterMap = JSONUtil.parseObj(milvusFilterOld); + if(ObjectUtil.isNotEmpty(filterMap.get("filterExpression"))){ + milvusFilterOld = filterMap.get("filterExpression").toString(); + } + if(ObjectUtil.isNotEmpty(filterMap.get("vectorValue"))){ + vectorValue = filterMap.get("vectorValue").toString(); + } + if(ObjectUtil.isNotEmpty(filterMap.get("sMethodName"))){ + bMethodName = BooleanUtil.toBoolean(filterMap.get("sMethodName").toString()); + } + } + Boolean milvusFilterCheck = milvusService.isStringFilterValid(milvusFilterOld,sInputTabelName); + milvusFilter = milvusFilterCheck?milvusFilterOld : null; + if(!bMethodName && ObjectUtil.isEmpty(vectorValue) && ObjectUtil.isEmpty(milvusFilter)){ + return resultExplain; + } } Integer pageSize = 100; if(ObjectUtil.isEmpty(milvusFilter)){ pageSize = 10; } // 待条件全查 不带 10条 - List> data = milvusService.getDataToCollection(sInputTabelName, milvusFilter,userInput,pageSize,filedsShow); + List> data = milvusService.getDataToCollection(sInputTabelName, milvusFilter,userInput,pageSize,filedsShow, vectorValue,sceneName); //存储到历史问题库(带where条件了就不存)并且没有记录过缓存 if(!bCach && ObjectUtil.isEmpty(milvusFilter)){ //执行操作记录表 @@ -436,13 +459,7 @@ public class XlyErpService { } } //采用表格形式显示明细、...详情、...记录、...列表、...清单 - if(ObjectUtil.isEmpty(milvusFilter) - || userInput.contains("明细") - || userInput.contains("详情") - || userInput.contains("记录") - || userInput.contains("列表") - || userInput.contains("清单") - ){ + if( retrunMarkdownType(userInput) ){ resultExplain = buildMarkdownTableWithStream(data, title); }else{ resultExplain = aiAgent.explainMilvusResult(session.getUserId(),userInput,sMilvusFiledDescriptionAll,JSONObject.toJSONString(data)); @@ -453,6 +470,21 @@ public class XlyErpService { } return resultExplain; } + + /*** + * @Author 钱豹 + * @Date 19:48 2026/3/28 + * @Param [userInput] + * @return java.lang.Boolean + * @Description 是否返回Markdown类型 + **/ + private Boolean retrunMarkdownType(String userInput){ + return userInput.contains("明细") +// || userInput.contains("详情") +// || userInput.contains("记录") + || userInput.contains("列表") + || userInput.contains("清单"); + } /*** * @Author 钱豹 * @Date 13:19 2026/3/25 @@ -640,38 +672,47 @@ public class XlyErpService { if(Integer.valueOf(iErroCount)>0){ doAiSqlErrorHistoryThread(session, cleanSql, StrUtil.EMPTY, StrUtil.EMPTY,input); } - //插入常用操作 + //插入常用操作 不包含where 条件 if(doAddSql){ //执行操作记录表 doAiUserAgentQuestion(session,input,cleanSql,"MYSQL",chatMessage); } - String sText = aiAgent.explainSqlResult( - userId, - userInput, - cleanSql, - tableStruct, - resultJson - ); - return sText; + //采用表格形式显示明细、...详情、...记录、...列表、...清单 + String resultExplain = StrUtil.EMPTY; + if(retrunMarkdownType(userInput) ){ + List> titles = getMarkdownTableTitleWithSql(sqlResult); + resultExplain = buildMarkdownTableWithStream(sqlResult, titles); + }else { + resultExplain = aiAgent.explainSqlResult( + userId, + userInput, + cleanSql, + tableStruct, + resultJson); + } + return resultExplain; } /*** * @Author 钱豹 - * @Date 17:04 2026/3/19 - * @Param [session] - * @return java.lang.String - * @Description 获取动态SQL(历史中查询) + * @Date 19:55 2026/3/28 + * @Param [sqlResult] + * @return java.util.List> + * @Description 动态SQL 返回Markdown 形式抬头 **/ - private Map getDynamicTableCach(UserSceneSession session,String input){ - try{ - String searchText = session.getCurrentScene().getSId()+"_"+session.getCurrentTool().getSId()+input; - //根据问题查询向量库 - Map serMap = aiGlobalAgentQuestionSqlEmitterService.queryAiGlobalAgentQuestionSqlEmitter(searchText, "ai_global_agent_question_sql"); - return serMap; - }catch (Exception e){ - log.error("取是否走缓存异常"); + private List> getMarkdownTableTitleWithSql(List> sqlResult){ + if(ObjectUtil.isEmpty(sqlResult)){ + return new ArrayList<>(); } - return null; + Map one = sqlResult.get(0); + List> titleData = new ArrayList<>(); + one.forEach((k,v)->{ + Map title = new HashMap<>(); + title.put("sTitle",k); + title.put("sName",k); + titleData.add(title); + }); + return titleData; } /*** @@ -681,22 +722,14 @@ public class XlyErpService { * @return java.lang.String * @Description 获取动态SQL(历史中查询) **/ - private String getDynamicTableNl2Sql(UserSceneSession session,String input){ -// String sReidKey = SqlValidateUtil.getsKey( session.getCurrentScene().getSId(), session.getCurrentTool().getSId(), input); -// Object sSql = redisService.get(sReidKey); -// if(ObjectUtil.isNotEmpty(sSql)){ -// return sSql.toString(); -// } + private Map getDynamicTableCach(UserSceneSession session,String input){ try{ String searchText = session.getCurrentScene().getSId()+"_"+session.getCurrentTool().getSId()+input; - //SqlValidateUtil.getsKey( session.getCurrentScene().getSId(), session.getCurrentTool().getSId(), SqlValidateUtil.getsQuestion(session.getSUserQuestionList())); //根据问题查询向量库 Map serMap = aiGlobalAgentQuestionSqlEmitterService.queryAiGlobalAgentQuestionSqlEmitter(searchText, "ai_global_agent_question_sql"); - if(ObjectUtil.isNotEmpty(serMap)){ - return serMap.get("sSqlContent").toString(); - } + return serMap; }catch (Exception e){ - + log.error("取是否走缓存异常"); } return null; } diff --git a/src/main/java/com/xly/thread/AiUserAgentQuestionThread.java b/src/main/java/com/xly/thread/AiUserAgentQuestionThread.java index 82fa20c..93367fd 100644 --- a/src/main/java/com/xly/thread/AiUserAgentQuestionThread.java +++ b/src/main/java/com/xly/thread/AiUserAgentQuestionThread.java @@ -9,8 +9,8 @@ import com.xly.entity.UserSceneSession; import com.xly.milvus.service.AiGlobalAgentQuestionSqlEmitterService; import com.xly.service.DynamicExeDbService; import com.xly.service.RedisService; -import com.xly.util.MD5Util; import com.xly.util.SqlValidateUtil; +import com.xly.util.SqlWhereHelper; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.ChatMessageType; import java.util.HashMap; @@ -54,17 +54,15 @@ public class AiUserAgentQuestionThread implements Runnable { } String sKey = sSceneId+"_"+sMethodId +"_"+sQuestion; // SqlValidateUtil.getsKey( sSceneId, sMethodId, SqlValidateUtil.getsQuestion(session.getSUserQuestionList())); - //存入向量库 - aiGlobalAgentQuestionSqlEmitterService.addAiGlobalAgentQuestionSqlEmitter(sKey,data,sQuestion,sSqlContent,cachType,"ai_global_agent_question_sql"); - //调用数据库插入数据库 + //存入向量库 不包含where 条件 + if(!SqlWhereHelper.hasWhereCondition(sSqlContent)){ + aiGlobalAgentQuestionSqlEmitterService.addAiGlobalAgentQuestionSqlEmitter(sKey,data,sQuestion,sSqlContent,cachType,"ai_global_agent_question_sql"); + } + //调用数据库插入数据库 Map searMap = dynamicExeDbService.getDoProMap(sProName, data); dynamicExeDbService.getCallPro(searMap, sProName); } - - - - //获取组ID private String getQuestionGroupNo(){ String sQuestionGroupNo = userMessage.stream() diff --git a/src/main/java/com/xly/util/SqlWhereHelper.java b/src/main/java/com/xly/util/SqlWhereHelper.java new file mode 100644 index 0000000..9b5ee71 --- /dev/null +++ b/src/main/java/com/xly/util/SqlWhereHelper.java @@ -0,0 +1,134 @@ +package com.xly.util; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.update.Update; + +@Slf4j +public class SqlWhereHelper { + + /** + * 判断 SQL 是否包含 WHERE 条件(使用 JSqlParser) + */ + public static boolean hasWhereCondition(String sql) { + if (StrUtil.isBlank(sql)) { + return false; + } + try { + Statement statement = CCJSqlParserUtil.parse(sql); + if (statement instanceof Select) { + Select select = (Select) statement; + return select.getPlainSelect() != null && select.getPlainSelect().getWhere() != null; + } + if (statement instanceof Update) { + Update update = (Update) statement; + return update.getWhere() != null; + } + if (statement instanceof Delete) { + Delete delete = (Delete) statement; + return delete.getWhere() != null; + } + } catch (JSQLParserException e) { + log.warn("SQL 解析失败,回退到简单匹配: {}", sql, e); + return hasWhereConditionSimple(sql); + } + return false; + } + + /** + * 简单判断是否有 WHERE + */ + public static boolean hasWhereConditionSimple(String sql) { + if (StrUtil.isBlank(sql)) return false; + String s = sql.toUpperCase(); + int whereIdx = s.indexOf("WHERE"); + if (whereIdx == -1) return false; + + String before = s.substring(0, whereIdx).trim(); + if (!before.matches(".*\\b(SELECT|UPDATE|DELETE)\\b.*")) return false; + + String after = s.substring(whereIdx + 5).trim(); + return !after.isEmpty() && !after.matches("^(GROUP BY|ORDER BY|LIMIT).*"); + } + + // ===================== 你要的新方法 ===================== + /** + * 判断: + * 1. 有 WHERE + * 2. 条件中 不包含 = > < >= <= IN EXISTS + * → 满足返回 true + */ + public static boolean hasWhereButNoCompareOperators(String sql) { + if (!hasWhereCondition(sql)) { + return false; + } + + String upper = sql.toUpperCase(); + + // 禁止出现的条件符号/关键字 + boolean hasEq = upper.contains("="); + boolean hasGt = upper.contains(">"); + boolean hasLt = upper.contains("<"); + boolean hasIn = upper.contains(" IN "); + boolean hasExists = upper.contains(" EXISTS "); + + // 只要有任何一个,就返回 false + if (hasEq || hasGt || hasLt || hasIn || hasExists) { + return false; + } + + // 有 WHERE 且 无等值/区间/大小/IN/EXISTS → 返回 true + return true; + } + + // ===================== 你原来的方法不动 ===================== + public static int getWhereInsertPosition(String sql) { + if (hasWhereCondition(sql)) { + return -1; + } + String upperSql = sql.toUpperCase(); + int fromIndex = upperSql.indexOf("FROM"); + if (fromIndex == -1) return sql.length(); + + int tableEndIndex = findTableEnd(sql, fromIndex + 4); + int groupIndex = upperSql.indexOf("GROUP BY", tableEndIndex); + int orderIndex = upperSql.indexOf("ORDER BY", tableEndIndex); + int limitIndex = upperSql.indexOf("LIMIT", tableEndIndex); + + int nextKeyword = sql.length(); + if (groupIndex != -1) nextKeyword = Math.min(nextKeyword, groupIndex); + if (orderIndex != -1) nextKeyword = Math.min(nextKeyword, orderIndex); + if (limitIndex != -1) nextKeyword = Math.min(nextKeyword, limitIndex); + return nextKeyword; + } + + private static int findTableEnd(String sql, int startIndex) { + if (startIndex >= sql.length()) return sql.length(); + String afterFrom = sql.substring(startIndex); + int endIndex = startIndex; + for (int i = 0; i < afterFrom.length(); i++) { + char c = afterFrom.charAt(i); + if (Character.isWhitespace(c) || c == ',' || c == '(' || c == ')') { + endIndex = startIndex + i; + break; + } + endIndex = startIndex + i + 1; + } + return endIndex; + } + + public static String addWhereCondition(String sql, String condition) { + if (StrUtil.isBlank(sql) || StrUtil.isBlank(condition)) return sql; + if (hasWhereCondition(sql)) { + return sql + " AND " + condition; + } else { + int insertPos = getWhereInsertPosition(sql); + return sql.substring(0, insertPos) + " WHERE " + condition + sql.substring(insertPos); + } + } +} \ No newline at end of file -- libgit2 0.22.2