Commit fce865d46b450ce838d63cd34dbe2871bd6f967c

Authored by yanghl
2 parents 35313021 cff9d5c5

Merge remote-tracking branch 'origin/master'

Showing 39 changed files with 4368 additions and 234 deletions

Too many changes to show.

To preserve performance only 35 of 39 files are displayed.

... ... @@ -39,7 +39,7 @@
39 39 <jackson.version>2.17.2</jackson.version>
40 40 <json-schema.version>1.17.2</json-schema.version>
41 41 <!-- 向量数据 -->
42   - <weaviate.version>4.4.0</weaviate.version>
  42 + <milvus.version>2.6.15</milvus.version>
43 43 </properties>
44 44  
45 45 <dependencies>
... ... @@ -49,6 +49,30 @@
49 49 <artifactId>spring-boot-starter-web</artifactId>
50 50 </dependency>
51 51  
  52 + <!-- Milvus Java SDK 核心依赖 -->
  53 + <dependency>
  54 + <groupId>io.milvus</groupId>
  55 + <artifactId>milvus-sdk-java</artifactId>
  56 + <version>${milvus.version}</version>
  57 + </dependency>
  58 +
  59 + <dependency>
  60 + <groupId>dev.langchain4j</groupId>
  61 + <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
  62 + <version>${langchain4j.version}</version>
  63 + </dependency>
  64 +
  65 +<!-- <dependency>-->
  66 +<!-- <groupId>com.baomidou</groupId>-->
  67 +<!-- <artifactId>mybatis-plus-boot-starter</artifactId>-->
  68 +<!-- <version>3.5.3.1</version>-->
  69 +<!-- </dependency>-->
  70 +
  71 + <dependency>
  72 + <groupId>org.springframework.boot</groupId>
  73 + <artifactId>spring-boot-starter-actuator</artifactId>
  74 + </dependency>
  75 +
52 76 <dependency>
53 77 <groupId>org.springframework.boot</groupId>
54 78 <artifactId>spring-boot-starter-validation</artifactId>
... ... @@ -63,6 +87,12 @@
63 87 <artifactId>spring-boot-starter-aop</artifactId>
64 88 </dependency>
65 89  
  90 + <dependency>
  91 + <groupId>com.google.code.gson</groupId>
  92 + <artifactId>gson</artifactId>
  93 + <version>2.10.1</version>
  94 + </dependency>
  95 +
66 96 <!-- Spring Cloud Context 依赖 -->
67 97 <dependency>
68 98 <groupId>org.springframework.cloud</groupId>
... ... @@ -270,69 +300,12 @@
270 300 <version>1.3.2</version>
271 301 </dependency>
272 302  
273   - <!-- H2 Database -->
274   - <!-- 最新稳定版本 -->
275   - <!-- <dependency>-->
276   - <!-- <groupId>com.h2database</groupId>-->
277   - <!-- <artifactId>h2</artifactId>-->
278   - <!-- <version>2.2.224</version>-->
279   - <!-- </dependency>-->
280   -
281   - <!-- 数据验证 -->
282   - <!-- <dependency>-->
283   - <!-- <groupId>org.hibernate.orm</groupId>-->
284   - <!-- <artifactId>hibernate-core</artifactId>-->
285   - <!-- <version>6.4.4.Final</version>-->
286   - <!-- </dependency>-->
287   - <!-- <dependency>-->
288   - <!-- <groupId>org.hibernate.validator</groupId>-->
289   - <!-- <artifactId>hibernate-validator</artifactId>-->
290   - <!-- <version>8.0.1.Final</version>-->
291   - <!-- </dependency>-->
292 303 <dependency>
293 304 <groupId>jakarta.persistence</groupId>
294 305 <artifactId>jakarta.persistence-api</artifactId>
295 306 <version>3.1.0</version>
296 307 </dependency>
297 308  
298   -
299   - <!-- <dependency>-->
300   - <!-- <groupId>dev.langchain4j</groupId>-->
301   - <!-- <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>-->
302   - <!-- <version>${langchain4jpdf.version}</version>-->
303   - <!-- </dependency>-->
304   -
305   -
306   - <!-- 工具调用 -->
307   - <!-- <dependency>-->
308   - <!-- <groupId>dev.langchain4j</groupId>-->
309   - <!-- <artifactId>langchain4j</artifactId>-->
310   - <!-- <version>${langchain4-10.version}</version>-->
311   - <!-- </dependency>-->
312   -
313   - <!-- ========== 官方 SDK(可选) ========== -->
314   - <!-- OpenAI 官方 SDK -->
315   - <!-- <dependency>-->
316   - <!-- <groupId>com.theokanning.openai-gpt3-java</groupId>-->
317   - <!-- <artifactId>service</artifactId>-->
318   - <!-- <version>${openai-java.version}</version>-->
319   - <!-- </dependency>-->
320   -
321   - <!-- &lt;!&ndash; Anthropic 官方 SDK &ndash;&gt;-->
322   - <!-- <dependency>-->
323   - <!-- <groupId>com.anthropics</groupId>-->
324   - <!-- <artifactId>anthropic-sdk-java</artifactId>-->
325   - <!-- <version>${anthropic-sdk.version}</version>-->
326   - <!-- </dependency>-->
327   -
328   - <!-- 嵌入向量 -->
329   - <!-- <dependency>-->
330   - <!-- <groupId>dev.langchain4j</groupId>-->
331   - <!-- <artifactId>langchain4j-embeddings</artifactId>-->
332   - <!-- <version>${langchain4jbeta.version}</version>-->
333   - <!-- </dependency>-->
334   -
335   -
336 309 <!-- ========== LangChain4j 1.10.0 ========== -->
337 310 <!-- 核心库 -->
338 311 <dependency>
... ... @@ -341,23 +314,13 @@
341 314 <version>${langchain4j.version}</version>
342 315 </dependency>
343 316  
344   - <!-- OpenAI 集成(支持 Function Calling) -->
345   -<!-- <dependency>-->
346   -<!-- <groupId>dev.langchain4j</groupId>-->
347   -<!-- <artifactId>langchain4j-open-ai</artifactId>-->
348   -<!-- <version>${langchain4j.version}</version>-->
349   -<!-- </dependency>-->
350 317  
351 318 <dependency>
352 319 <groupId>dev.langchain4j</groupId>
353 320 <artifactId>langchain4j</artifactId>
354 321 <version>${langchain4j.version}</version>
355 322 </dependency>
356   - <!-- <dependency>-->
357   - <!-- <groupId>dev.langchain4j</groupId>-->
358   - <!-- <artifactId>langchain4j-embeddings</artifactId>-->
359   - <!-- <version>${langchain4j.version}</version>-->
360   - <!-- </dependency>-->
  323 +
361 324 <dependency>
362 325 <groupId>dev.langchain4j</groupId>
363 326 <artifactId>langchain4j-ollama</artifactId>
... ... @@ -371,13 +334,6 @@
371 334 <version>1.17.0</version>
372 335 </dependency>
373 336  
374   - <!-- 文档加载器 -->
375   - <!-- Tika 文档解析器(支持多种格式) -->
376   - <!-- <dependency>-->
377   - <!-- <groupId>dev.langchain4j</groupId>-->
378   - <!-- <artifactId>langchain4j-document-parser-tika</artifactId>-->
379   - <!-- <version>${langchain4j.version}</version>-->
380   - <!-- </dependency>-->
381 337  
382 338 <!-- 或者使用 Apache Tika 直接 -->
383 339 <dependency>
... ... @@ -392,12 +348,6 @@
392 348 <version>2.9.1</version>
393 349 </dependency>
394 350  
395   - <!-- 嵌入向量 -->
396   - <!-- <dependency>-->
397   - <!-- <groupId>dev.langchain4j</groupId>-->
398   - <!-- <artifactId>langchain4j-embeddings</artifactId>-->
399   - <!-- <version>${langchain4jbeta.version}</version>-->
400   - <!-- </dependency>-->
401 351  
402 352 <!-- Spring Retry -->
403 353 <dependency>
... ...
src/main/java/com/xly/agent/ErpAiAgent.java
... ... @@ -27,10 +27,13 @@ public interface ErpAiAgent {
27 27 * 入参:用户问题、执行的SQL、表结构、JSON格式结果
28 28 */
29 29 @SystemMessage("""
30   - 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景:
  30 + 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景:
31 31 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇;
32 32 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致;
33   - 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据”;
  33 + 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据”
  34 + 3.1. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 1000000,错误写法是 1,000,000,即使数字很大,也请保持连续的数字串,不要打断。
  35 + 3.2 所有日期请转换为 YYYY-MM-DD 格式(例如:2026-03-15),严禁包含时间部分(如小时、分钟、秒)(例如:2026-03-15 00:00:00),也不要包含时区信息。”
  36 + 3.3. 金额,单价,数量 严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 2400056,错误写法是 2,400,056 即使数字很大,也请保持连续的数字串,不要打断。
34 37 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势;
35 38 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。
36 39 """)
... ... @@ -48,4 +51,132 @@ public interface ErpAiAgent {
48 51 @V("sql") String sql,
49 52 @V("tableStruct") String tableStruct,
50 53 @V("result") String result);
  54 +
  55 + /**
  56 + * 动态表结构:自然语言解释SQL执行结果
  57 + * 入参:用户问题、执行的SQL、表结构、JSON格式结果
  58 + */
  59 + @SystemMessage("""
  60 + 你是专业的业务数据分析师,请分析以下查询结果:
  61 + 【用户问题】
  62 + {{userInput}}
  63 + 【数据字段说明】
  64 + {{sMilvusFiledDescription}}
  65 + 【查询结果数据(JSON格式)】
  66 + {{result}}
  67 + 【分析要求】
  68 + 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇;
  69 + 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致;
  70 + 3. 输出格式:
  71 + 3.1. 如果用户要求"表格形式展示",先输出简短文字说明,然后输出Markdown格式的表格
  72 + 3.2. 如果用户未要求表格,仅返回解释内容,不要列出ID,无多余标题、换行、符号
  73 + 3.3. 结果为空时直接返回"未查询到相关数据"
  74 + 3.4. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",")
  75 + 3.5. 所有日期请转换为 YYYY-MM-DD 格式,严禁包含时间部分
  76 + 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势;
  77 + 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。
  78 + """)
  79 + @UserMessage("""
  80 + 【用户查询】
  81 + {{userInput}}
  82 + 【字段说明】
  83 + {{sMilvusFiledDescription}}
  84 + 【查询结果】
  85 + 用户原始查询:{{userInput}}
  86 + 执行查询向量库后结果(JSON格式):{{result}}
  87 + 请根据上述信息+通用规则,对查询结果做业务解释:
  88 + """)
  89 + String explainMilvusResult(@MemoryId String userId,
  90 + @V("userInput") String userInput,
  91 + @V("sMilvusFiledDescription") String sMilvusFiledDescription,
  92 + @V("result") String result);
  93 +
  94 + /**
  95 + * AI路由判断接口
  96 + * true: 走聚合查询(MySQL)
  97 + * false: 走向量检索(Milvus)
  98 + */
  99 +// @SystemMessage("""
  100 +// 你是一个智能查询路由专家,请根据用户需求判断应该使用哪种查询方式。
  101 +//
  102 +// 判断标准:
  103 +// 1. 返回 true(聚合查询/MySQL)的场景:
  104 +// - 需要计算统计指标:总数、总和、平均值、最大/最小值、占比
  105 +// - 需要数据汇总:分组统计、排行榜、TopN
  106 +// - 包含关键词:统计、求和、汇总、排名、平均、数量、总额、最高、最低、占比、分组、分析、趋势
  107 +// - 示例:统计本月销售总额、查询销量前10的商品、各品类占比分析
  108 +//
  109 +// 2. 返回 false(向量检索/Milvus)的场景:
  110 +// - 查询明细数据:XXX的销售订单明细、XXX的客户信息、具体内容详情
  111 +// - 查找相似内容:根据语义查找相关文档、推荐相似商品
  112 +// - 模糊匹配:不确定具体关键词,需要语义理解
  113 +// - 内容检索:查找包含特定概念的文档
  114 +// - 包含关键词:明细、详情、查询明细、查找、搜索、匹配、推荐、相似、相关、类似
  115 +// - 示例:李留记的销售订单明细、查询关于人工智能的文档、找相似的图片
  116 +//
  117 +// 重要规则:
  118 +// - 只返回 true 或 false,不要返回其他内容
  119 +// - 不要解释,不要添加额外文字
  120 +// - 如果用户要求"表格形式展示",返回 false(明细查询)
  121 +// - 如果用户指定具体人名、具体对象,返回 false(明细查询)
  122 +// """)
  123 +// @UserMessage("用户需求:{{userInput}}")
  124 + @SystemMessage("""
  125 + 你是一个智能查询路由专家。请根据【用户需求】,只返回 true 或 false
  126 + - 如果用户需求包含以下关键词:统计、求和、汇总、排名、TopN、平均、数量、总额、最高、最低、占比、分组,则返回true
  127 + - 如果用户需求属于模糊匹配、普通语义检索,查询明细,(例如:查询报价单明细,查询客户信息),则返回false
  128 + - 查询明细数据:XXX的销售订单明细、XXX的客户信息、具体内容详情,则返回false
  129 + - 模糊匹配:不确定具体关键词,需要语义理解,则返回false
  130 + """)
  131 + @UserMessage("""
  132 + 【用户需求】
  133 + {{userInput}}
  134 + """)
  135 + Boolean routeQuery(@MemoryId String userId, @V("userInput") String userInput);
  136 +
  137 + /**
  138 + * 生成 Milvus 过滤条件
  139 + */
  140 + @SystemMessage("""
  141 + MILVUS 标量过滤条件生成规则(严格遵守):
  142 + 1. 语法规范:
  143 + - 允许的操作符:==, !=, like
  144 + - 逻辑组合:&& (AND), || (OR)
  145 + - 所有字段都是字符串类型,值必须使用单引号包裹
  146 + - 字符串中的单引号需要转义:'O''Reilly'
  147 + 2. 可用字段(只能使用这些字段):
  148 + - {{sMilvusFiled}}
  149 + 字段说明:
  150 + - {{sMilvusFiledDescription}}
  151 + 3. 重要规则:
  152 + - 只使用上述可用字段,不要创建新字段
  153 + - 如果用户提到了文档类型(如"报价单"、"订单"等),但可用字段中没有类型字段,则忽略该条件
  154 + - 只提取有明确值的字段条件
  155 + 4. 生成规则:
  156 + - 如果没有提取到任何具体条件,返回空字符串
  157 + - 从用户输入中提取明确的字段条件
  158 + - 识别模式:字段名 + 操作符 + 值
  159 + - 示例:
  160 + * "单据号 INV001" → sBillNo == 'INV001'
  161 + * "客户编号 C001" → sCustomerNo == 'C001'
  162 + * "销售人员张三" → sSalesManName == '张三'
  163 + * "产品包含手机" → sProductStyle like '%手机%'
  164 + 5. 输出格式:
  165 + - 仅返回纯过滤条件,无任何解释、换行、备注
  166 + - 单条件:sBillNo == 'INV001'
  167 + - 多条件:(sBillNo == 'INV001' && sCustomerNo == 'C001')
  168 + - 无条件:直接返回空字符串
  169 + """)
  170 + @UserMessage("""
  171 + 【用户查询】
  172 + - {{userInput}}
  173 + 【可用字段】
  174 + - {{sMilvusFiled}}
  175 + 【字段说明】
  176 + - {{sMilvusFiledDescription}}
  177 + """)
  178 + String getMilvusFilter(@MemoryId String userId,
  179 + @V("userInput") String userInput,
  180 + @V("sMilvusFiled") String sMilvusFiled,
  181 + @V("sMilvusFiledDescription") String sMilvusFiledDescription);
51 182 }
... ...
src/main/java/com/xly/config/OperableChatMemoryProvider.java
... ... @@ -121,6 +121,30 @@ public class OperableChatMemoryProvider implements ChatMemoryProvider {
121 121 // 步骤4: 完全重新设置消息列表
122 122 return rebuildMemoryWithMessages(memoryId, currentMessages);
123 123 }
  124 +
  125 + public List<ChatMessage> deleteUserLasterMessageBySize(Object memoryId,Integer size) {
  126 + if (Objects.isNull(memoryId) || size==0) {
  127 + return getCurrentChatMessages(memoryId);
  128 + }
  129 + // 步骤1: 获取当前所有消息
  130 + ChatMemory currentMemory = this.get(memoryId);
  131 + List<ChatMessage> currentMessages = new ArrayList<>(currentMemory.messages());
  132 + // 从后往前查找内容匹配的最后一条消息
  133 + int indexToDelete = currentMessages.size();
  134 + // 如果找到匹配的消息
  135 + if (indexToDelete >= 0) {
  136 + List<ChatMessage> filteredMessages = new ArrayList<>(currentMessages);
  137 + for(int i=0;i<size;i++){
  138 + indexToDelete = indexToDelete -1;
  139 + if(indexToDelete>1){
  140 + filteredMessages.remove(indexToDelete);
  141 + }
  142 + }
  143 + return rebuildMemoryWithMessages(memoryId, filteredMessages);
  144 + }
  145 + // 步骤4: 完全重新设置消息列表
  146 + return rebuildMemoryWithMessages(memoryId, currentMessages);
  147 + }
124 148 /**
125 149 * 批量删除多条消息
126 150 * @param memoryId 会话ID
... ...
src/main/java/com/xly/entity/AiResponseAccumulator.java 0 → 100644
  1 +package com.xly.entity;
  2 +
  3 +import com.xly.entity.AiResponseDTO;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +
  7 +/**
  8 + * AI响应累积器
  9 + */
  10 +@Slf4j
  11 +public class AiResponseAccumulator {
  12 +
  13 + private final String requestId;
  14 + private final StringBuilder aiTextBuilder = new StringBuilder();
  15 + private final StringBuilder systemTextBuilder = new StringBuilder();
  16 + private String sSceneName;
  17 + private String sMethodName;
  18 + private String sReturnType;
  19 + private int totalChunks = 0;
  20 + private int processedChunks = 0;
  21 + private final long startTime;
  22 +
  23 + public AiResponseAccumulator(String requestId) {
  24 + this.requestId = requestId;
  25 + this.startTime = System.currentTimeMillis();
  26 + }
  27 +
  28 + /**
  29 + * 累积单个AI响应
  30 + */
  31 + public void accumulate(AiResponseDTO response) {
  32 + processedChunks++;
  33 +
  34 + // 更新总块数
  35 + if (response.getTotalChunks() != null && response.getTotalChunks() > 0) {
  36 + this.totalChunks = response.getTotalChunks();
  37 + }
  38 +
  39 + // 累积AI文本片段
  40 + if (StrUtil.isNotBlank(response.getTextFragment())) {
  41 + aiTextBuilder.append(response.getTextFragment());
  42 + }
  43 +
  44 + // 累积系统文本片段
  45 + if (StrUtil.isNotBlank(response.getSystemTextFragment())) {
  46 + systemTextBuilder.append(response.getSystemTextFragment());
  47 + }
  48 +
  49 + // 更新元数据(取最后一次非空值)
  50 + if (StrUtil.isNotBlank(response.getSSceneName())) {
  51 + this.sSceneName = response.getSSceneName();
  52 + }
  53 + if (StrUtil.isNotBlank(response.getSMethodName())) {
  54 + this.sMethodName = response.getSMethodName();
  55 + }
  56 + if (StrUtil.isNotBlank(response.getSReturnType())) {
  57 + this.sReturnType = response.getSReturnType();
  58 + }
  59 +
  60 + log.debug("累积进度: requestId={}, 已处理={}/{}块, AI文本长度={}",
  61 + requestId, processedChunks, totalChunks, aiTextBuilder.length());
  62 + }
  63 +
  64 + /**
  65 + * 获取完整的响应
  66 + */
  67 + public AiResponseDTO getCompleteResponse() {
  68 + AiResponseDTO response = new AiResponseDTO();
  69 + response.setRequestId(requestId);
  70 + response.setAiText(aiTextBuilder.toString());
  71 + response.setSystemText(systemTextBuilder.toString());
  72 + response.setFullAiText(aiTextBuilder.toString());
  73 + response.setFullSystemText(systemTextBuilder.toString());
  74 + response.setSSceneName(sSceneName);
  75 + response.setSMethodName(sMethodName);
  76 + response.setSReturnType(sReturnType != null ? sReturnType : "MARKDOWN");
  77 + response.setTotalChunks(totalChunks);
  78 + response.setChunkIndex(processedChunks - 1);
  79 + response.setIsLastChunk(true);
  80 + response.setElapsedTime(System.currentTimeMillis() - startTime);
  81 +
  82 + log.info("ERP累积完成: requestId={}, 总块数={}, AI文本长度={}, 系统文本长度={}, 耗时={}ms",
  83 + requestId, totalChunks,
  84 + aiTextBuilder.length(), systemTextBuilder.length(),
  85 + response.getElapsedTime());
  86 +
  87 + return response;
  88 + }
  89 +}
0 90 \ No newline at end of file
... ...
src/main/java/com/xly/entity/AiResponseDTO.java
... ... @@ -9,9 +9,12 @@ import lombok.Data;
9 9 import lombok.NoArgsConstructor;
10 10  
11 11 import java.io.Serializable;
  12 +import java.util.List;
  13 +import java.util.Map;
12 14  
13 15 /**
14 16 * TTS响应数据传输对象
  17 + * 增强版:支持流式处理
15 18 */
16 19 @Data
17 20 @Builder
... ... @@ -20,14 +23,230 @@ import java.io.Serializable;
20 23 public class AiResponseDTO implements Serializable {
21 24  
22 25 private static final long serialVersionUID = 1L;
23   - // AI文字部分
  26 +
  27 + // ============ 原有字段 ============
  28 +
  29 + /**
  30 + * AI文字部分
  31 + */
24 32 private String aiText;
25   - //系统拼接返回的文字部分
  33 +
  34 + /**
  35 + * 系统拼接返回的文字部分
  36 + */
26 37 private String systemText;
27   - //业务场景名称
  38 +
  39 + /**
  40 + * 业务场景名称
  41 + */
28 42 private String sSceneName;
29   - //业务方法名称
  43 +
  44 + /**
  45 + * 业务方法名称
  46 + */
30 47 private String sMethodName;
  48 +
  49 + /**
  50 + * 返回类型,默认MARKDOWN
  51 + */
31 52 private String sReturnType = ReturnTypeCode.MAKEDOWN.getCode();
32 53  
  54 + // ============ 新增字段:流式处理支持 ============
  55 +
  56 + /**
  57 + * 请求ID,用于追踪整个流式请求
  58 + */
  59 + private String requestId;
  60 +
  61 + /**
  62 + * 响应码
  63 + */
  64 + private Integer code;
  65 +
  66 + /**
  67 + * 响应消息
  68 + */
  69 + private String message;
  70 +
  71 + /**
  72 + * 处理状态:PROCESSING, COMPLETED, FAILED
  73 + */
  74 + private String status;
  75 +
  76 + /**
  77 + * 当前处理的块编号(从0开始)
  78 + */
  79 + private Integer chunkIndex;
  80 +
  81 + /**
  82 + * 总块数
  83 + */
  84 + private Integer totalChunks;
  85 +
  86 + /**
  87 + * 是否是最后一块
  88 + */
  89 + private Boolean isLastChunk;
  90 +
  91 + /**
  92 + * 文本片段(用于流式传输)
  93 + * 当aiText过大时,可以分段传输
  94 + */
  95 + private String textFragment;
  96 +
  97 + /**
  98 + * 系统文本片段(用于流式传输)
  99 + */
  100 + private String systemTextFragment;
  101 +
  102 + /**
  103 + * 累积的完整AI文本(仅在最后一块时返回)
  104 + */
  105 + private String fullAiText;
  106 +
  107 + /**
  108 + * 累积的完整系统文本(仅在最后一块时返回)
  109 + */
  110 + private String fullSystemText;
  111 +
  112 + /**
  113 + * 处理进度(0-100)
  114 + */
  115 + private Integer progress;
  116 +
  117 + /**
  118 + * 时间戳
  119 + */
  120 + private Long timestamp;
  121 +
  122 + /**
  123 + * 处理耗时(毫秒)
  124 + */
  125 + private Long elapsedTime;
  126 +
  127 + /**
  128 + * 扩展字段,用于存储额外的元数据
  129 + */
  130 + private Map<String, Object> metadata;
  131 +
  132 + /**
  133 + * 错误信息(当status=FAILED时)
  134 + */
  135 + private String errorMessage;
  136 +
  137 + /**
  138 + * 错误码(当status=FAILED时)
  139 + */
  140 + private String errorCode;
  141 +
  142 + // ============ 便捷方法 ============
  143 +
  144 + /**
  145 + * 判断是否处理成功
  146 + */
  147 + public boolean isSuccess() {
  148 + return code != null && code == 200;
  149 + }
  150 +
  151 + /**
  152 + * 判断是否处理中
  153 + */
  154 + public boolean isProcessing() {
  155 + return "PROCESSING".equals(status);
  156 + }
  157 +
  158 + /**
  159 + * 判断是否已完成
  160 + */
  161 + public boolean isCompleted() {
  162 + return "COMPLETED".equals(status);
  163 + }
  164 +
  165 + /**
  166 + * 判断是否失败
  167 + */
  168 + public boolean isFailed() {
  169 + return "FAILED".equals(status);
  170 + }
  171 +
  172 + /**
  173 + * 获取完整的文本(AI文本 + 系统文本)
  174 + */
  175 + public String getFullText() {
  176 + StringBuilder sb = new StringBuilder();
  177 + if (StrUtil.isNotBlank(aiText)) {
  178 + sb.append(aiText);
  179 + }
  180 + if (StrUtil.isNotBlank(systemText)) {
  181 + sb.append(systemText);
  182 + }
  183 + return sb.toString();
  184 + }
  185 +
  186 + /**
  187 + * 创建处理中的响应
  188 + */
  189 + public static AiResponseDTO processing(String requestId, String textFragment,
  190 + Integer chunkIndex, Integer totalChunks) {
  191 + return AiResponseDTO.builder()
  192 + .requestId(requestId)
  193 + .code(200)
  194 + .message("Processing")
  195 + .status("PROCESSING")
  196 + .textFragment(textFragment)
  197 + .chunkIndex(chunkIndex)
  198 + .totalChunks(totalChunks)
  199 + .timestamp(System.currentTimeMillis())
  200 + .progress(calculateProgress(chunkIndex, totalChunks))
  201 + .build();
  202 + }
  203 +
  204 + /**
  205 + * 创建完成响应
  206 + */
  207 + public static AiResponseDTO completed(String requestId, String fullAiText,
  208 + String fullSystemText, String sSceneName,
  209 + String sMethodName, String sReturnType) {
  210 + return AiResponseDTO.builder()
  211 + .requestId(requestId)
  212 + .code(200)
  213 + .message("Completed")
  214 + .status("COMPLETED")
  215 + .fullAiText(fullAiText)
  216 + .fullSystemText(fullSystemText)
  217 + .aiText(fullAiText)
  218 + .systemText(fullSystemText)
  219 + .sSceneName(sSceneName)
  220 + .sMethodName(sMethodName)
  221 + .sReturnType(sReturnType)
  222 + .progress(100)
  223 + .timestamp(System.currentTimeMillis())
  224 + .isLastChunk(true)
  225 + .build();
  226 + }
  227 +
  228 + /**
  229 + * 创建失败响应
  230 + */
  231 + public static AiResponseDTO failed(String requestId, String errorMessage, String errorCode) {
  232 + return AiResponseDTO.builder()
  233 + .requestId(requestId)
  234 + .code(500)
  235 + .message("Failed")
  236 + .status("FAILED")
  237 + .errorMessage(errorMessage)
  238 + .errorCode(errorCode)
  239 + .timestamp(System.currentTimeMillis())
  240 + .build();
  241 + }
  242 +
  243 + /**
  244 + * 计算进度
  245 + */
  246 + private static Integer calculateProgress(Integer chunkIndex, Integer totalChunks) {
  247 + if (chunkIndex == null || totalChunks == null || totalChunks == 0) {
  248 + return 0;
  249 + }
  250 + return (int) ((chunkIndex + 1) * 100.0 / totalChunks);
  251 + }
33 252 }
34 253 \ No newline at end of file
... ...
src/main/java/com/xly/entity/ToolMeta.java
... ... @@ -47,4 +47,7 @@ public class ToolMeta {
47 47 private List<ParamRule> paramRuleListCheck;//需要校验
48 48 private List<ParamRule> paramRuleListAll;//所有的
49 49 private LocalDateTime tMakeDate;
  50 + private String sVectorfiled;
  51 + private String sVectorjson;
  52 +
50 53 }
... ...
src/main/java/com/xly/milvus/bean/CustomSearchResultsWrapper.java 0 → 100644
  1 +package com.xly.milvus.bean;
  2 +
  3 +import io.milvus.grpc.SearchResultData;
  4 +import io.milvus.response.SearchResultsWrapper;
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * 自定义SearchResultsWrapper,用于访问protected方法
  9 + */
  10 +public class CustomSearchResultsWrapper extends SearchResultsWrapper {
  11 +
  12 + public CustomSearchResultsWrapper(SearchResultData results) {
  13 + super(results);
  14 + }
  15 +
  16 + /**
  17 + * 公开访问getOutputFields方法
  18 + */
  19 + public List<String> getOutputFieldsPublic() {
  20 + return super.getOutputFields();
  21 + }
  22 +}
0 23 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/bean/SimilaritySearchRequest.java 0 → 100644
  1 +package com.xly.milvus.bean;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Builder;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +import java.util.List;
  9 +import java.util.Map;
  10 +
  11 +/**
  12 + * 相似度查询请求实体
  13 + */
  14 +@Data
  15 +@Builder
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class SimilaritySearchRequest {
  19 + private List<Float> queryVector; // 查询向量
  20 + private String queryText; // 查询文本(如果有文本转向量服务)
  21 + private Integer topK = 10; // 返回数量
  22 + private Double minScore; // 最小相似度得分
  23 + private Double maxScore; // 最大相似度得分
  24 + private String metricType = "IP"; // 距离类型: IP(内积), L2(欧氏距离), COSINE(余弦)
  25 + private List<String> outputFields; // 输出字段
  26 + private Map<String, Object> filter; // 过滤条件
  27 + private String partitionName; // 分区名称
  28 + private Boolean withScore = true; // 是否返回得分
  29 + private Boolean withDistance = false; // 是否返回距离
  30 +}
0 31 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/bean/SimilaritySearchResult.java 0 → 100644
  1 +package com.xly.milvus.bean;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Builder;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +import java.util.Map;
  9 +
  10 +/**
  11 + * 相似度查询结果实体 - 增强版
  12 + */
  13 +@Data
  14 +@Builder
  15 +@NoArgsConstructor
  16 +@AllArgsConstructor
  17 +public class SimilaritySearchResult {
  18 + private Long id; // 整数ID
  19 + private String strId; // 字符串ID
  20 + private Float score; // 相似度得分
  21 + private Double normalizedScore; // 归一化后的得分(0-1之间)
  22 + private Map<String, Object> fields; // 字段数据
  23 + private String collectionName; // 集合名称
  24 +
  25 + /**
  26 + * 获取ID(优先返回字符串ID,如果没有则返回整数ID)
  27 + */
  28 + public String getId() {
  29 + if (strId != null && !strId.isEmpty()) {
  30 + return strId;
  31 + }
  32 + return id != null ? String.valueOf(id) : null;
  33 + }
  34 +
  35 + /**
  36 + * 获取字段值
  37 + */
  38 + public Object getField(String fieldName) {
  39 + if (fields != null) {
  40 + return fields.get(fieldName);
  41 + }
  42 + return null;
  43 + }
  44 +
  45 + /**
  46 + * 获取字符串字段值
  47 + */
  48 + public String getStringField(String fieldName) {
  49 + Object value = getField(fieldName);
  50 + return value != null ? value.toString() : null;
  51 + }
  52 +
  53 + /**
  54 + * 获取整数字段值
  55 + */
  56 + public Integer getIntField(String fieldName) {
  57 + Object value = getField(fieldName);
  58 + if (value instanceof Number) {
  59 + return ((Number) value).intValue();
  60 + }
  61 + return null;
  62 + }
  63 +}
0 64 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/config/EmbeddingConfig.java 0 → 100644
  1 +package com.xly.milvus.config;
  2 +
  3 +import dev.langchain4j.model.embedding.EmbeddingModel;
  4 +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.context.annotation.Configuration;
  7 +
  8 +/**
  9 + * LangChain4j 嵌入模型配置类
  10 + */
  11 +@Configuration
  12 +public class EmbeddingConfig {
  13 +
  14 + @Bean
  15 + public EmbeddingModel embeddingModel() {
  16 + // 使用 All-MiniLM-L6-v2 嵌入模型
  17 + // 这是一个轻量级的模型,维度为384,适合在本地运行
  18 + return new AllMiniLmL6V2EmbeddingModel();
  19 + }
  20 +}
0 21 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/config/MilvusConfig.java 0 → 100644
  1 +package com.xly.milvus.config;
  2 +
  3 +import io.milvus.v2.client.ConnectConfig;
  4 +import io.milvus.v2.client.MilvusClientV2;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.beans.factory.annotation.Value;
  7 +import org.springframework.context.annotation.Bean;
  8 +import org.springframework.context.annotation.Configuration;
  9 +import org.springframework.retry.annotation.EnableRetry;
  10 +import org.springframework.retry.support.RetryTemplate;
  11 +
  12 +@Slf4j
  13 +@Configuration
  14 +@EnableRetry
  15 +public class MilvusConfig {
  16 +
  17 + @Value("${milvus.host:localhost}")
  18 + private String host;
  19 +
  20 + @Value("${milvus.port:19530}")
  21 + private Integer port;
  22 +
  23 + @Value("${milvus.database:default}")
  24 + private String database;
  25 +
  26 + @Value("${milvus.username:}")
  27 + private String username;
  28 +
  29 + @Value("${milvus.password:}")
  30 + private String password;
  31 +
  32 + @Value("${milvus.connect-timeout:10000}")
  33 + private Long connectTimeout;
  34 +
  35 + @Value("${milvus.rpc-deadline:10000}")
  36 + private Long rpcDeadline;
  37 +
  38 + @Value("${milvus.keep-alive-time:300000}")
  39 + private Long keepAliveTime;
  40 +
  41 + @Value("${milvus.keep-alive-timeout:5000}")
  42 + private Long keepAliveTimeout;
  43 +
  44 + @Value("${milvus.secure:false}")
  45 + private Boolean secure;
  46 +
  47 + @Value("${milvus.validate-on-startup:true}")
  48 + private boolean validateOnStartup;
  49 +
  50 + @Value("${milvus.fail-on-connect-error:false}")
  51 + private boolean failOnConnectError;
  52 +
  53 + @Value("${milvus.enable-precheck:true}")
  54 + private boolean enablePrecheck;
  55 +
  56 + @Bean(destroyMethod = "close")
  57 + public MilvusClientV2 milvusClient() {
  58 + try {
  59 + String uri = String.format("http://%s:%d", host, port);
  60 + // 构建连接配置
  61 + ConnectConfig.ConnectConfigBuilder configBuilder = ConnectConfig.builder()
  62 + .uri(uri)
  63 + .dbName("default")
  64 + .connectTimeoutMs(connectTimeout)
  65 + .rpcDeadlineMs(rpcDeadline)
  66 + .keepAliveTimeMs(keepAliveTime)
  67 + .keepAliveTimeoutMs(keepAliveTimeout)
  68 + .keepAliveWithoutCalls(true)
  69 + .secure(secure)
  70 + .enablePrecheck(enablePrecheck);
  71 +
  72 + // 添加认证信息(如果有)
  73 + if (username != null && !username.isEmpty()) {
  74 + configBuilder.username(username);
  75 + if (password != null) {
  76 + configBuilder.password(password);
  77 + }
  78 + }
  79 +
  80 + ConnectConfig connectConfig = configBuilder.build();
  81 +
  82 + // 创建客户端
  83 + MilvusClientV2 client = new MilvusClientV2(connectConfig);
  84 + // 检查xlyerp数据库是否存在,如果不存在则创建
  85 + var databases = client.listDatabases();
  86 + if (!databases.getDatabaseNames().contains(database)) {
  87 + log.info("数据库 xlyerp 不存在,正在创建...");
  88 + client.createDatabase(
  89 + io.milvus.v2.service.database.request.CreateDatabaseReq.builder()
  90 + .databaseName(database)
  91 + .build()
  92 + );
  93 + log.info("数据库 xlyerp 创建成功");
  94 + }
  95 +
  96 + // 切换到xlyerp数据库
  97 + client.useDatabase (database);
  98 + log.info("切换到数据库: {}",database);
  99 +
  100 + // 启动验证
  101 + if (validateOnStartup) {
  102 + validateConnection(client);
  103 + }
  104 + return client;
  105 + } catch (Exception e) {
  106 + log.error("创建Milvus客户端时发生异常: {}", e.getMessage(), e);
  107 + throw new RuntimeException("无法创建Milvus客户端", e);
  108 + }
  109 +
  110 + }
  111 +
  112 + /**
  113 + * 验证Milvus连接 - 基于源码中可用的方法
  114 + */
  115 + private void validateConnection(MilvusClientV2 client) {
  116 + try {
  117 + // 方法1: 检查服务器版本(源码中存在getServerVersion())
  118 + String serverVersion = client.getServerVersion();
  119 +
  120 + // 方法2: 列出数据库(源码中存在listDatabases())
  121 + var databases = client.listDatabases();
  122 +
  123 + // 方法3: 检查健康状态(源码中存在checkHealth())
  124 + var health = client.checkHealth();
  125 +
  126 + System.out.println("✅ Milvus连接成功");
  127 + System.out.println(" - 服务器版本: " + serverVersion);
  128 + System.out.println(" - 数据库: " + databases);
  129 + System.out.println(" - 健康状态: " + health);
  130 +
  131 + // 验证指定数据库是否存在
  132 + if (database != null && !database.isEmpty()) {
  133 + boolean dbExists = databases.getDatabaseNames().contains(database);
  134 + if (!dbExists && !"default".equals(database)) {
  135 + System.err.println("⚠️ 警告: 指定数据库 '" + database + "' 不存在");
  136 + }
  137 + }
  138 +
  139 + } catch (Exception e) {
  140 + String errorMsg = String.format("❌ Milvus连接失败: %s:%d - %s",host, port, e.getMessage());
  141 +
  142 + if (failOnConnectError) {
  143 + throw new IllegalStateException(errorMsg, e);
  144 + } else {
  145 + System.err.println(errorMsg);
  146 + System.err.println(" ⚠️ 应用将继续启动,但Milvus功能可能不可用");
  147 + }
  148 + }
  149 + }
  150 +
  151 + /**
  152 + * 配置重试模板(可选)
  153 + */
  154 + @Bean
  155 + public RetryTemplate milvusRetryTemplate() {
  156 + return RetryTemplate.builder()
  157 + .maxAttempts(3)
  158 + .exponentialBackoff(1000, 2, 10000)
  159 + .retryOn(Exception.class)
  160 + .build();
  161 + }
  162 +}
0 163 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/config/MilvusHealthIndicator.java 0 → 100644
  1 +package com.xly.milvus.config;
  2 +
  3 +import io.milvus.v2.client.MilvusClientV2;
  4 +import org.springframework.beans.factory.annotation.Autowired;
  5 +import org.springframework.boot.actuate.health.Health;
  6 +import org.springframework.boot.actuate.health.HealthIndicator;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import java.util.HashMap;
  10 +import java.util.Map;
  11 +
  12 +@Component
  13 +public class MilvusHealthIndicator implements HealthIndicator {
  14 +
  15 + @Autowired(required = false)
  16 + private MilvusClientV2 milvusClient;
  17 +
  18 + @Override
  19 + public Health health() {
  20 + if (milvusClient == null) {
  21 + return Health.down()
  22 + .withDetail("error", "Milvus客户端未初始化")
  23 + .build();
  24 + }
  25 +
  26 + try {
  27 + // 检查客户端是否就绪(源码中存在clientIsReady())
  28 + boolean isReady = milvusClient.clientIsReady();
  29 +
  30 + if (!isReady) {
  31 + return Health.down()
  32 + .withDetail("error", "客户端未就绪")
  33 + .build();
  34 + }
  35 +
  36 + long startTime = System.currentTimeMillis();
  37 +
  38 + // 执行健康检查 - 使用源码中存在的方法
  39 + String serverVersion = milvusClient.getServerVersion();
  40 + var databases = milvusClient.listDatabases();
  41 + var healthCheck = milvusClient.checkHealth();
  42 +
  43 + long responseTime = System.currentTimeMillis() - startTime;
  44 +
  45 + Map<String, Object> details = new HashMap<>();
  46 + details.put("serverVersion", serverVersion);
  47 + details.put("database", milvusClient.currentUsedDatabase());
  48 + details.put("databases", databases.getDatabaseNames());
  49 + details.put("healthStatus", healthCheck.getIsHealthy());
  50 + details.put("responseTime", responseTime + "ms");
  51 + details.put("clientReady", isReady);
  52 +
  53 + if (healthCheck.getQuotaStates() != null) {
  54 + details.put("quotaStates", healthCheck.getQuotaStates());
  55 + }
  56 +
  57 + return Health.up()
  58 + .withDetails(details)
  59 + .build();
  60 +
  61 + } catch (Exception e) {
  62 + return Health.down()
  63 + .withDetail("error", e.getMessage())
  64 + .withDetail("clientReady", milvusClient.clientIsReady())
  65 + .build();
  66 + }
  67 + }
  68 +}
0 69 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/config/MilvusRetryConfig.java 0 → 100644
  1 +package com.xly.milvus.config;
  2 +
  3 +import io.milvus.v2.client.RetryConfig;
  4 +import org.springframework.beans.factory.annotation.Value;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.context.annotation.Configuration;
  7 +
  8 +@Configuration
  9 +public class MilvusRetryConfig {
  10 +
  11 + @Value("${milvus.retry.max-retry-times:75}")
  12 + private int maxRetryTimes;
  13 +
  14 + @Value("${milvus.retry.initial-backoff-ms:10}")
  15 + private long initialBackOffMs;
  16 +
  17 + @Value("${milvus.retry.max-backoff-ms:3000}")
  18 + private long maxBackOffMs;
  19 +
  20 + @Value("${milvus.retry.backoff-multiplier:3}")
  21 + private int backOffMultiplier;
  22 +
  23 + @Value("${milvus.retry.retry-on-rate-limit:true}")
  24 + private boolean retryOnRateLimit;
  25 +
  26 + @Value("${milvus.retry.max-retry-timeout-ms:0}")
  27 + private long maxRetryTimeoutMs;
  28 +
  29 + @Value("${milvus.retry.enabled:true}")
  30 + private boolean retryEnabled;
  31 +
  32 + /**
  33 + * 方法名改为 createRetryConfig,避免与类名冲突
  34 + */
  35 + @Bean
  36 + public RetryConfig createRetryConfig() {
  37 + if (!retryEnabled) {
  38 + return RetryConfig.builder()
  39 + .maxRetryTimes(1)
  40 + .initialBackOffMs(0)
  41 + .maxBackOffMs(0)
  42 + .backOffMultiplier(1)
  43 + .retryOnRateLimit(false)
  44 + .maxRetryTimeoutMs(0)
  45 + .build();
  46 + }
  47 +
  48 + return RetryConfig.builder()
  49 + .maxRetryTimes(maxRetryTimes)
  50 + .initialBackOffMs(initialBackOffMs)
  51 + .maxBackOffMs(maxBackOffMs)
  52 + .backOffMultiplier(backOffMultiplier)
  53 + .retryOnRateLimit(retryOnRateLimit)
  54 + .maxRetryTimeoutMs(maxRetryTimeoutMs)
  55 + .build();
  56 + }
  57 +}
0 58 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/config/MilvusStartupValidator.java 0 → 100644
  1 +package com.xly.milvus.config;
  2 +
  3 +import io.milvus.v2.client.MilvusClientV2;
  4 +import io.milvus.v2.service.collection.request.HasCollectionReq;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.slf4j.Logger;
  7 +import org.slf4j.LoggerFactory;
  8 +import org.springframework.beans.factory.annotation.Autowired;
  9 +import org.springframework.beans.factory.annotation.Value;
  10 +import org.springframework.boot.ApplicationArguments;
  11 +import org.springframework.boot.ApplicationRunner;
  12 +import org.springframework.core.annotation.Order;
  13 +import org.springframework.stereotype.Component;
  14 +
  15 +import java.util.List;
  16 +
  17 +@Slf4j
  18 +@Component
  19 +@Order(1)
  20 +public class MilvusStartupValidator implements ApplicationRunner {
  21 +
  22 + @Autowired
  23 + private MilvusClientV2 milvusClient;
  24 +
  25 + @Value("${milvus.validation.collections:}")
  26 + private List<String> validationCollections;
  27 +
  28 + @Value("${milvus.validation.timeout:5000}")
  29 + private long validationTimeout;
  30 +
  31 + @Override
  32 + public void run(ApplicationArguments args) throws Exception {
  33 + log.info("开始Milvus启动验证...");
  34 +
  35 + try {
  36 + // 1. 验证客户端就绪状态
  37 + boolean isReady = milvusClient.clientIsReady();
  38 + log.info("客户端就绪状态: {}", isReady);
  39 +
  40 + // 2. 获取服务器版本
  41 + String serverVersion = milvusClient.getServerVersion();
  42 + log.info("Milvus服务器版本: {}", serverVersion);
  43 +
  44 + // 3. 列出所有数据库
  45 + var databases = milvusClient.listDatabases();
  46 + log.info("可用数据库: {}", databases.getDatabaseNames());
  47 +
  48 + // 4. 检查当前使用的数据库
  49 + String currentDb = milvusClient.currentUsedDatabase();
  50 + log.info("当前数据库: {}", currentDb);
  51 +
  52 + // 5. 健康检查
  53 + var healthCheck = milvusClient.checkHealth();
  54 + log.info("健康状态: {}", healthCheck.getIsHealthy());
  55 + if (healthCheck.getQuotaStates() != null && !healthCheck.getQuotaStates().isEmpty()) {
  56 + log.info("配额状态: {}", healthCheck.getQuotaStates());
  57 + }
  58 +
  59 + // 6. 验证指定的集合(可选)
  60 + if (validationCollections != null && !validationCollections.isEmpty()) {
  61 + for (String collectionName : validationCollections) {
  62 + boolean exists = milvusClient.hasCollection(
  63 + HasCollectionReq.builder()
  64 + .collectionName(collectionName)
  65 + .build()
  66 + );
  67 + log.info("集合 '{}' 存在: {}", collectionName, exists);
  68 + }
  69 + }
  70 +
  71 + log.info("Milvus启动验证完成");
  72 +
  73 + } catch (Exception e) {
  74 + log.error("Milvus启动验证失败: {}", e.getMessage(), e);
  75 + // 可以根据配置决定是否抛出异常
  76 + // throw new RuntimeException("Milvus验证失败", e);
  77 + }
  78 + }
  79 +}
0 80 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/config/SearchResult.java 0 → 100644
  1 +package com.xly.milvus.config;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Builder;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +import java.util.Map;
  9 +
  10 +/**
  11 + * 搜索结果实体
  12 + */
  13 +@Data
  14 +@Builder
  15 +@NoArgsConstructor
  16 +@AllArgsConstructor
  17 +public class SearchResult {
  18 + private Long id;
  19 + private Float score;
  20 + private Map<String, Object> fields;
  21 + private String collectionName;
  22 +}
0 23 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/service/AiGlobalAgentQuestionSqlEmitterService.java 0 → 100644
  1 +package com.xly.milvus.service;
  2 +
  3 +
  4 +import java.util.Map;
  5 +
  6 +public interface AiGlobalAgentQuestionSqlEmitterService {
  7 +
  8 + /***
  9 + * @Author 钱豹
  10 + * @Date 15:27 2026/3/19
  11 + * @Param [data, sQuestion, sSqlContent, collectionName]
  12 + * @return void
  13 + * @Description 插入向量库
  14 + **/
  15 + void addAiGlobalAgentQuestionSqlEmitter(String sKey,Map<String,Object> data, String sQuestion, String sSqlContent, String collectionName);
  16 +
  17 +
  18 + Map<String, Object> queryAiGlobalAgentQuestionSqlEmitter(String searchText, String collectionName);
  19 +
  20 +}
0 21 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/service/EmbeddingService.java 0 → 100644
  1 +package com.xly.milvus.service;
  2 +
  3 +import java.util.List;
  4 +
  5 +public interface EmbeddingService {
  6 +
  7 + /**
  8 + * 生成单个文本的向量
  9 + */
  10 + public List<Float> generateEmbedding(String text);
  11 +
  12 + /**
  13 + * 批量生成向量(高效版)
  14 + * 利用 LangChain4j 内置的并行化能力,显著提升性能
  15 + */
  16 + public List<List<Float>> generateEmbeddings(List<String> texts);
  17 +}
0 18 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/service/MilvusService.java 0 → 100644
  1 +package com.xly.milvus.service;
  2 +
  3 +import com.xly.tts.bean.TTSResponseDTO;
  4 +
  5 +import java.util.ArrayList;
  6 +import java.util.HashMap;
  7 +import java.util.List;
  8 +import java.util.Map;
  9 +
  10 +/**
  11 + * 向量化服务接口
  12 + */
  13 +public interface MilvusService {
  14 +
  15 + /***
  16 + * @Author 钱豹
  17 + * @Date 22:17 2026/3/24
  18 + * @Param
  19 + * @return
  20 + * @Description 初始化数据
  21 + **/
  22 +
  23 + TTSResponseDTO initDataToMilvus(Map<String, Object> reqMap);
  24 +
  25 +
  26 + /**
  27 + * 创建集合(如果不存在)
  28 + */
  29 + void createCollectionIfNotExists(String collectionName, String sVectorfiled, String sVectorjson, Boolean bRset);
  30 +
  31 +
  32 + /***
  33 + * @Author 钱豹
  34 + * @Date 21:39 2026/3/24
  35 + * @Param [collectionName, sVectorfiled, sVectorjson, data]
  36 + * @return long
  37 + * @Description 批量插入数据
  38 + **/
  39 + long addDataToCollection(String collectionName, String sVectorfiled, String sVectorjson, List<Map<String, Object>> data);
  40 +
  41 + /***
  42 + * @Author 钱豹
  43 + * @Date 10:39 2026/3/25
  44 + * @Param
  45 + * @return
  46 + * @Description 向量库查询
  47 + **/
  48 + List<Map<String, Object>> getDataToCollection(String collectionName, String milvusFilter,String searchText,Integer size,List<String> fields);
  49 +
  50 + /***
  51 + * @Author 钱豹
  52 + * @Date 10:56 2026/3/25
  53 + * @Param [sVectorfiled]
  54 + * @return java.util.Map<java.lang.String,java.lang.Object>
  55 + * @Description 获取配置
  56 + **/
  57 + Map<String,Object> getMilvusFiled(String sVectorfiled);
  58 +}
0 59 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/service/VectorizationService.java 0 → 100644
  1 +package com.xly.milvus.service;
  2 +
  3 +import java.util.List;
  4 +
  5 +/**
  6 + * 向量化服务接口
  7 + */
  8 +public interface VectorizationService {
  9 +
  10 + /**
  11 + * 将文本向量化
  12 + * @param text 文本内容
  13 + * @return 向量数组
  14 + */
  15 + List<Float> textToVector(String text);
  16 +
  17 + /**
  18 + * 批量向量化
  19 + * @param texts 文本列表
  20 + * @return 向量列表
  21 + */
  22 + List<List<Float>> batchTextToVector(List<String> texts);
  23 +}
0 24 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/service/impl/AiGlobalAgentQuestionSqlEmitterServiceImpl.java 0 → 100644
  1 +package com.xly.milvus.service.impl;
  2 +
  3 +import cn.hutool.core.collection.ConcurrentHashSet;
  4 +import cn.hutool.core.util.ObjectUtil;
  5 +import com.google.gson.JsonArray;
  6 +import com.google.gson.JsonObject;
  7 +import com.xly.milvus.service.AiGlobalAgentQuestionSqlEmitterService;
  8 +import com.xly.milvus.service.VectorizationService;
  9 +import com.xly.milvus.util.MapToJsonConverter;
  10 +import io.milvus.v2.client.MilvusClientV2;
  11 +import io.milvus.v2.common.ConsistencyLevel;
  12 +import io.milvus.v2.common.DataType;
  13 +import io.milvus.v2.common.IndexBuildState;
  14 +import io.milvus.v2.common.IndexParam;
  15 +import io.milvus.v2.service.collection.request.*;
  16 +import io.milvus.v2.service.index.request.CreateIndexReq;
  17 +import io.milvus.v2.service.index.request.DescribeIndexReq;
  18 +import io.milvus.v2.service.index.request.DropIndexReq;
  19 +import io.milvus.v2.service.index.response.DescribeIndexResp;
  20 +import io.milvus.v2.service.vector.request.InsertReq;
  21 +import io.milvus.v2.service.vector.request.SearchReq;
  22 +import io.milvus.v2.service.vector.request.data.FloatVec;
  23 +import io.milvus.v2.service.vector.response.InsertResp;
  24 +import io.milvus.v2.service.vector.response.SearchResp;
  25 +import lombok.RequiredArgsConstructor;
  26 +import lombok.extern.slf4j.Slf4j;
  27 +import org.springframework.beans.factory.annotation.Value;
  28 +import org.springframework.stereotype.Service;
  29 +
  30 +import java.util.*;
  31 +
  32 +@Slf4j
  33 +@Service("aiGlobalAgentQuestionSqlEmitterService")
  34 +@RequiredArgsConstructor
  35 +public class AiGlobalAgentQuestionSqlEmitterServiceImpl implements AiGlobalAgentQuestionSqlEmitterService {
  36 +
  37 + private final MilvusClientV2 milvusClient;
  38 + private final VectorizationService vectorizationService;
  39 +
  40 + // 或者从配置文件读取
  41 + @Value("${milvus.vector.dimension:384}")
  42 + private int VECTOR_DIM;
  43 +
  44 + // 缓存已加载的集合
  45 + private final Set<String> loadedCollections = new ConcurrentHashSet<>();
  46 +
  47 + /***
  48 + * @Author 钱豹
  49 + * @Date 13:06 2026/3/19
  50 + * @Param []
  51 + * @return void
  52 + * @Description 插入数据
  53 + **/
  54 + @Override
  55 + public void addAiGlobalAgentQuestionSqlEmitter(String sKey,Map<String,Object> data,String sQuestion,String sSqlContent,String collectionName) {
  56 + // 向量化
  57 + List<Float> vector = vectorizationService.textToVector(sKey);
  58 +
  59 + if (vector == null || vector.isEmpty()) {
  60 + throw new RuntimeException("向量化失败");
  61 + }
  62 +
  63 + // 2. 转换为Milvus格式
  64 + JsonObject row = convertToMilvusRow(data, vector,sQuestion,sSqlContent,sKey);
  65 +
  66 + //创建集合
  67 +// createCollection(collectionName);
  68 + createCollectionIfNotExists(collectionName);
  69 +
  70 + // 3. 插入到Milvus
  71 + InsertReq insertReq = InsertReq.builder()
  72 + .collectionName(collectionName)
  73 + .data(List.of(row))
  74 + .build();
  75 +
  76 + InsertResp insertResp = milvusClient.insert(insertReq);
  77 + System.out.println("成功插入 " + insertResp.getInsertCnt() + " 条数据");
  78 + System.out.println(" - 数据预览:");
  79 + }
  80 +
  81 + @Override
  82 + public Map<String, Object> queryAiGlobalAgentQuestionSqlEmitter(String searchText, String collectionName) {
  83 + Map<String, Object> result = new HashMap<>();
  84 + log.info("开始相似度查询: collection={}, searchText={}", collectionName, searchText);
  85 + // 2. 设置范围搜索参数
  86 + Map<String, Object> searchParams = new HashMap<>();
  87 + searchParams.put("nprobe", 10);
  88 + // 对于 IP 度量,相似度范围在 [minScore, maxScore]
  89 + searchParams.put("radius", 0.9); // 最小相似度
  90 + searchParams.put("range_filter", 1); // 最大相似度
  91 + // 1. 确保集合已加载
  92 + ensureCollectionLoaded(collectionName);
  93 +
  94 + // 1. 向量化搜索文本
  95 + List<Float> vectorList = vectorizationService.textToVector(searchText);
  96 + if (vectorList == null || vectorList.isEmpty()) {
  97 + throw new RuntimeException("向量化失败");
  98 + }
  99 + // 2. 转换为 float[]
  100 + float[] floatArray = new float[vectorList.size()];
  101 + for (int i = 0; i < vectorList.size(); i++) {
  102 + floatArray[i] = vectorList.get(i);
  103 + }
  104 + // 查询最近插入的数据(按时间倒序)
  105 +// QueryReq queryReq = QueryReq.builder()
  106 +// .collectionName(collectionName)
  107 +// .outputFields(Arrays.asList("sQuestion", "sSqlContent", "data_id", "create_time","metadata"))
  108 +// .limit(100)
  109 +// .build();
  110 +// QueryResp queryResp = milvusClient.query(queryReq);
  111 +
  112 + // 3. 创建 Milvus FloatVec 对象
  113 + FloatVec floatVec = new FloatVec(floatArray);
  114 + // 4. 构建搜索请求
  115 + SearchReq searchReq = SearchReq.builder()
  116 + .collectionName(collectionName)
  117 + .data(Collections.singletonList(floatVec))
  118 + .annsField("vector") // 向量字段名
  119 + .topK(10) // 返回最相似的10条
  120 + .metricType(IndexParam.MetricType.IP) // 内积相似度
  121 + .outputFields(Arrays.asList("sQuestion", "sSqlContent", "data_id", "create_time","metadata"))
  122 + .searchParams(searchParams)
  123 + .build();
  124 + // 5. 执行搜索
  125 + SearchResp searchResp = milvusClient.search(searchReq);
  126 + // 6. 处理结果
  127 + List<List<SearchResp.SearchResult>> searchResults = searchResp.getSearchResults();
  128 + if(ObjectUtil.isEmpty(searchResults)){
  129 + return result;
  130 + }
  131 + List<SearchResp.SearchResult> firstResultList = searchResults.get(0);
  132 + if(ObjectUtil.isEmpty(firstResultList)){
  133 + return result;
  134 + }
  135 + firstResultList.sort((a, b) -> Float.compare(b.getScore(), a.getScore()));
  136 + SearchResp.SearchResult item = firstResultList.get(0);
  137 + Map<String, Object> itemMap = new HashMap<>();
  138 + itemMap.put("score", item.getScore());
  139 + itemMap.put("id", item.getId());
  140 + itemMap.putAll(item.getEntity());
  141 + return itemMap;
  142 + }
  143 +
  144 + /**
  145 + * 确保集合已加载
  146 + */
  147 + private void ensureCollectionLoaded(String collectionName) {
  148 + try {
  149 + // 如果已经加载过,直接返回
  150 + if (loadedCollections.contains(collectionName)) {
  151 + return;
  152 + }
  153 + log.info("检查集合加载状态: {}", collectionName);
  154 + // 检查集合是否存在
  155 + HasCollectionReq hasCollectionReq = HasCollectionReq.builder()
  156 + .collectionName(collectionName)
  157 + .build();
  158 +
  159 + boolean exists = milvusClient.hasCollection(hasCollectionReq);
  160 +
  161 + if (!exists) {
  162 + log.error("集合不存在: {}", collectionName);
  163 + throw new RuntimeException("集合不存在: " + collectionName);
  164 + }
  165 +
  166 + // 获取加载状态
  167 + GetLoadStateReq getLoadStateReq = GetLoadStateReq.builder()
  168 + .collectionName(collectionName)
  169 + .build();
  170 +
  171 + boolean isLoaded = milvusClient.getLoadState(getLoadStateReq);
  172 +
  173 + if (!isLoaded) {
  174 + log.info("加载集合到内存: {}", collectionName);
  175 +
  176 + // 加载集合
  177 + LoadCollectionReq loadCollectionReq = LoadCollectionReq.builder()
  178 + .collectionName(collectionName)
  179 + .build();
  180 +
  181 + milvusClient.loadCollection(loadCollectionReq);
  182 +
  183 + // 等待加载完成
  184 + waitForCollectionLoaded(collectionName);
  185 +
  186 + loadedCollections.add(collectionName);
  187 + log.info("集合加载完成: {}", collectionName);
  188 + } else {
  189 + loadedCollections.add(collectionName);
  190 + log.info("集合已加载: {}", collectionName);
  191 + }
  192 +
  193 + } catch (Exception e) {
  194 + log.error("确保集合加载失败", e);
  195 + throw new RuntimeException("集合加载失败: " + collectionName, e);
  196 + }
  197 + }
  198 +
  199 + /**
  200 + * 等待集合加载完成
  201 + */
  202 + private void waitForCollectionLoaded(String collectionName) {
  203 + int maxRetries = 30;
  204 + int retryInterval = 1000; // 1秒
  205 +
  206 + for (int i = 0; i < maxRetries; i++) {
  207 + try {
  208 + GetLoadStateReq getLoadStateReq = GetLoadStateReq.builder()
  209 + .collectionName(collectionName)
  210 + .build();
  211 +
  212 + boolean isLoaded = milvusClient.getLoadState(getLoadStateReq);
  213 +
  214 + if (isLoaded) {
  215 + log.info("集合加载状态确认: {}", collectionName);
  216 + return;
  217 + }
  218 +
  219 + Thread.sleep(retryInterval);
  220 +
  221 + } catch (Exception e) {
  222 + log.warn("检查加载状态失败,重试 {}/{}", i + 1, maxRetries);
  223 + }
  224 + }
  225 +
  226 + throw new RuntimeException("集合加载超时: " + collectionName);
  227 + }
  228 +
  229 +
  230 + /**
  231 + * 从实体对象构建Milvus插入数据
  232 + */
  233 + public JsonObject convertToMilvusRow(Map<String,Object> data, List<Float> vector,String sQuestion,String sSqlContent,String sKey) {
  234 + JsonObject row = new JsonObject();
  235 +
  236 + // 添加向量
  237 + JsonArray vectorArray = new JsonArray();
  238 + vector.forEach(vectorArray::add);
  239 + row.add("vector", vectorArray);
  240 + // 添加文本字段
  241 + row.addProperty("sKey", sKey);
  242 + row.addProperty("data_id", data.get("sId").toString());
  243 + row.addProperty("sQuestion", sQuestion);
  244 + row.addProperty("sSqlContent", sSqlContent);
  245 + // 创建时间字段 - 必须提供!
  246 + row.addProperty("create_time", System.currentTimeMillis());
  247 + // 创建时间字段 - 必须提供!
  248 +// row.add("create_time", JsonValue.from(System.currentTimeMillis()));
  249 + // 添加业务字段到metadata
  250 + JsonObject metadata = MapToJsonConverter.convert(data);
  251 + row.add("metadata", metadata);
  252 + return row;
  253 + }
  254 +
  255 + /**
  256 + * 创建集合(如果不存在)
  257 + */
  258 + public void createCollectionIfNotExists(String collectionName) {
  259 + try {
  260 + // 检查集合是否存在
  261 + HasCollectionReq hasCollectionReq = HasCollectionReq.builder()
  262 + .collectionName(collectionName)
  263 + .build();
  264 + boolean exists = milvusClient.hasCollection(hasCollectionReq);
  265 + if (!exists) {
  266 + createCollection(collectionName);
  267 + log.info("集合 {} 创建成功", collectionName);
  268 + }
  269 + } catch (Exception e) {
  270 + log.error("检查/创建集合失败: {}", collectionName, e);
  271 + throw new RuntimeException("初始化Milvus集合失败", e);
  272 + }
  273 + }
  274 +
  275 + /**
  276 + * 创建集合(定义字段结构)
  277 + */
  278 + private void createCollection(String collectionName) {
  279 + //删除现有集合
  280 +// DropCollectionReq dropCollectionReq = DropCollectionReq.builder()
  281 +// .collectionName(collectionName)
  282 +// .build();
  283 +// milvusClient.dropCollection(dropCollectionReq);
  284 +
  285 + // 定义字段列表
  286 + List<CreateCollectionReq.FieldSchema> fieldSchemas = Arrays.asList(
  287 + // 1. 主键字段
  288 + CreateCollectionReq.FieldSchema.builder()
  289 + .name("id")
  290 + .dataType(DataType.Int64)
  291 + .isPrimaryKey(true)
  292 + .autoID(true) // 使用自动ID
  293 + .description("主键ID")
  294 + .build(),
  295 +
  296 + // 2. 向量字段
  297 + CreateCollectionReq.FieldSchema.builder()
  298 + .name("vector")
  299 + .dataType(DataType.FloatVector)
  300 + .dimension(VECTOR_DIM)
  301 + .description("向量字段,用于相似性搜索")
  302 + .build(),
  303 +
  304 + // 3. 问题字段
  305 + CreateCollectionReq.FieldSchema.builder()
  306 + .name("sQuestion")
  307 + .dataType(DataType.VarChar)
  308 + .maxLength(1000)
  309 + .description("用户问题")
  310 + .build(),
  311 +
  312 + // 4. SQL内容字段
  313 + CreateCollectionReq.FieldSchema.builder()
  314 + .name("sSqlContent")
  315 + .dataType(DataType.VarChar)
  316 + .maxLength(5000) // SQL可能较长
  317 + .description("SQL语句")
  318 + .build(),
  319 +
  320 + // 5. 数据ID字段
  321 + CreateCollectionReq.FieldSchema.builder()
  322 + .name("data_id")
  323 + .dataType(DataType.VarChar)
  324 + .maxLength(100)
  325 + .description("原始数据ID")
  326 + .build(),
  327 +
  328 + // 6. 创建时间字段
  329 + CreateCollectionReq.FieldSchema.builder()
  330 + .name("create_time")
  331 + .dataType(DataType.Int64)
  332 + .description("创建时间戳")
  333 + .build(),
  334 +
  335 + // 7. 元数据字段(使用JSON类型存储额外数据)
  336 + CreateCollectionReq.FieldSchema.builder()
  337 + .name("metadata")
  338 + .dataType(DataType.JSON)
  339 + .description("额外元数据")
  340 + .build(),
  341 + CreateCollectionReq.FieldSchema.builder()
  342 + .name("sKey")
  343 + .dataType(DataType.VarChar)
  344 + .maxLength(100)
  345 + .description("存入的vector转换前数据")
  346 + .build()
  347 + );
  348 +
  349 + // 创建集合schema
  350 + CreateCollectionReq.CollectionSchema schema =
  351 + CreateCollectionReq.CollectionSchema.builder()
  352 + .fieldSchemaList(fieldSchemas)
  353 + .enableDynamicField(true)
  354 + .build();
  355 +
  356 + // 创建集合请求
  357 + CreateCollectionReq createCollectionReq = CreateCollectionReq.builder()
  358 + .collectionName(collectionName)
  359 + .collectionSchema(schema)
  360 + .consistencyLevel(ConsistencyLevel.BOUNDED)
  361 + .build();
  362 +
  363 + // 执行创建集合
  364 + milvusClient.createCollection(createCollectionReq);
  365 +
  366 + //创建索引
  367 + createIndexesStepByStep(collectionName);
  368 + }
  369 +
  370 + /*
  371 + * 分步创建索引,便于监控每个索引的状态
  372 + */
  373 + private void createIndexesStepByStep(String collectionName) {
  374 + log.info("开始为集合创建索引: {}", collectionName);
  375 + createAllIndexes(collectionName);
  376 +// // 1. 创建向量索引
  377 +// createVectorIndex(collectionName);
  378 +//
  379 +// // 2. 创建标量索引
  380 +// createScalarIndexes(collectionName);
  381 + }
  382 +
  383 + /**
  384 + * 创建向量索引
  385 + * IVF_FLAT 向量相似度搜索 常用的向量索引,平衡性能和召回率
  386 + * STL_SORT 标量字段排序 适用于数字、时间等需要排序的字段
  387 + * INVERTED 文本字段过滤 倒排索引,适用于文本字段的精确匹配
  388 + * TRIE 字符串前缀匹配 适用于前缀查询
  389 + * BITMAP 枚举值过滤 适用于低基数字段
  390 + */
  391 + private void createVectorIndex(String collectionName) {
  392 + log.info("创建向量索引: {}", collectionName);
  393 +
  394 + Map<String, Object> extraParams = new HashMap<>();
  395 + extraParams.put("nlist", 128);
  396 +
  397 + IndexParam vectorIndex = IndexParam.builder()
  398 + .fieldName("vector")
  399 + .indexName("idx_vector")
  400 + .indexType(IndexParam.IndexType.IVF_FLAT)
  401 + .metricType(IndexParam.MetricType.IP)
  402 + .extraParams(extraParams)
  403 + .build();
  404 +
  405 + CreateIndexReq createIndexReq = CreateIndexReq.builder()
  406 + .collectionName(collectionName)
  407 + .indexParams(Collections.singletonList(vectorIndex))
  408 + .sync(true)
  409 + .timeout(60000L)
  410 + .build();
  411 +
  412 + milvusClient.createIndex(createIndexReq);
  413 + log.info("向量索引创建完成");
  414 + }
  415 +
  416 + /**
  417 + * 创建标量索引
  418 + */
  419 + private void createScalarIndexes(String collectionName) {
  420 + log.info("创建标量索引: {}", collectionName);
  421 +
  422 + // 为 create_time 字段创建索引
  423 + IndexParam timeIndex = IndexParam.builder()
  424 + .fieldName("create_time")
  425 + .indexName("idx_create_time")
  426 + .indexType(IndexParam.IndexType.STL_SORT) // 排序索引
  427 + .build();
  428 +
  429 + CreateIndexReq timeIndexReq = CreateIndexReq.builder()
  430 + .collectionName(collectionName)
  431 + .indexParams(Collections.singletonList(timeIndex))
  432 + .sync(true)
  433 + .timeout(30000L)
  434 + .build();
  435 +
  436 + milvusClient.createIndex(timeIndexReq);
  437 + log.info("create_time 索引创建完成");
  438 +
  439 + // 为 question 字段创建倒排索引(支持文本过滤)
  440 + IndexParam questionIndex = IndexParam.builder()
  441 + .fieldName("sQuestion")
  442 + .indexName("idx_question")
  443 + .indexType(IndexParam.IndexType.TRIE) // 倒排索引
  444 + .build();
  445 +
  446 + CreateIndexReq questionIndexReq = CreateIndexReq.builder()
  447 + .collectionName(collectionName)
  448 + .indexParams(Collections.singletonList(questionIndex))
  449 + .sync(true)
  450 + .timeout(30000L)
  451 + .build();
  452 +
  453 + milvusClient.createIndex(questionIndexReq);
  454 + log.info("question 索引创建完成");
  455 +
  456 + // 为 data_id 字段创建索引
  457 + IndexParam idIndex = IndexParam.builder()
  458 + .fieldName("data_id")
  459 + .indexName("idx_data_id")
  460 + .indexType(IndexParam.IndexType.TRIE)
  461 + .build();
  462 +
  463 + CreateIndexReq idIndexReq = CreateIndexReq.builder()
  464 + .collectionName(collectionName)
  465 + .indexParams(Collections.singletonList(idIndex))
  466 + .sync(true)
  467 + .timeout(30000L)
  468 + .build();
  469 +
  470 + milvusClient.createIndex(idIndexReq);
  471 + log.info("data_id 索引创建完成");
  472 + }
  473 +
  474 + /**
  475 + * 重建索引(解决索引未就绪问题)
  476 + */
  477 + public boolean rebuildIndex(String collectionName) {
  478 + log.info("========== 开始重建索引 ==========");
  479 + log.info("集合名称: {}", collectionName);
  480 +
  481 + try {
  482 + // 1. 先检查集合是否存在
  483 + HasCollectionReq hasReq = HasCollectionReq.builder()
  484 + .collectionName(collectionName)
  485 + .build();
  486 +
  487 + if (!milvusClient.hasCollection(hasReq)) {
  488 + log.error("集合不存在: {}", collectionName);
  489 + return false;
  490 + }
  491 +
  492 + // 2. 先释放集合(如果已加载)
  493 + try {
  494 + ReleaseCollectionReq releaseReq = ReleaseCollectionReq.builder()
  495 + .collectionName(collectionName)
  496 + .build();
  497 + milvusClient.releaseCollection(releaseReq);
  498 + log.info("集合已释放: {}", collectionName);
  499 + Thread.sleep(2000); // 等待释放完成
  500 + } catch (Exception e) {
  501 + log.warn("释放集合失败(可能未加载): {}", e.getMessage());
  502 + }
  503 +
  504 + // 3. 删除原有索引
  505 + try {
  506 + DropIndexReq dropIndexReq = DropIndexReq.builder()
  507 + .collectionName(collectionName)
  508 + .fieldName("vector") // 指定向量字段
  509 + .build();
  510 +
  511 + milvusClient.dropIndex(dropIndexReq);
  512 + log.info("原有索引已删除");
  513 + Thread.sleep(2000); // 等待删除完成
  514 + } catch (Exception e) {
  515 + log.warn("删除索引失败(可能不存在): {}", e.getMessage());
  516 + }
  517 +
  518 + // 4. 创建新索引
  519 + createVectorIndex(collectionName);
  520 +
  521 + // 5. 等待索引就绪
  522 + boolean indexReady = waitForIndexReady(collectionName, "vector", 60);
  523 + if (!indexReady) {
  524 + log.error("索引未就绪");
  525 + return false;
  526 + }
  527 +
  528 + // 6. 重新加载集合
  529 + LoadCollectionReq loadReq = LoadCollectionReq.builder()
  530 + .collectionName(collectionName)
  531 + .build();
  532 +
  533 + milvusClient.loadCollection(loadReq);
  534 + log.info("集合重新加载成功");
  535 +
  536 + // 7. 验证加载状态
  537 + boolean loaded = waitForLoad(collectionName, 30);
  538 + if (!loaded) {
  539 + log.error("集合加载失败");
  540 + return false;
  541 + }
  542 +
  543 + log.info("✅ 索引重建完成,集合已就绪: {}", collectionName);
  544 + return true;
  545 +
  546 + } catch (Exception e) {
  547 + log.error("重建索引失败", e);
  548 + return false;
  549 + }
  550 + }
  551 +
  552 + /**
  553 + * 等待索引就绪
  554 + */
  555 + private boolean waitForIndexReady(String collectionName, String fieldName, int timeoutSeconds) {
  556 + log.info("等待索引就绪: {}.{},超时: {}秒", collectionName, fieldName, timeoutSeconds);
  557 +
  558 + for (int i = 0; i < timeoutSeconds; i++) {
  559 + try {
  560 + DescribeIndexReq describeIndexReq = DescribeIndexReq.builder()
  561 + .collectionName(collectionName)
  562 + .build();
  563 +
  564 + DescribeIndexResp describeIndexResp = milvusClient.describeIndex(describeIndexReq);
  565 +
  566 + List<DescribeIndexResp.IndexDesc> indexDescs = describeIndexResp.getIndexDescriptions();
  567 +
  568 + for (DescribeIndexResp.IndexDesc desc : indexDescs) {
  569 + if (fieldName.equals(desc.getFieldName())) {
  570 + IndexBuildState state = desc.getIndexState();
  571 +
  572 + log.info("索引状态: {}, 进度: {}/{}",
  573 + state, desc.getIndexedRows(), desc.getTotalRows());
  574 +
  575 + if (state == IndexBuildState.Finished) {
  576 + log.info("✅ 索引就绪");
  577 + return true;
  578 + } else if (state == IndexBuildState.Failed) {
  579 + log.error("❌ 索引构建失败: {}", desc.getIndexFailedReason());
  580 + return false;
  581 + }
  582 + break;
  583 + }
  584 + }
  585 +
  586 + Thread.sleep(1000);
  587 +
  588 + } catch (InterruptedException e) {
  589 + Thread.currentThread().interrupt();
  590 + log.warn("等待被中断");
  591 + return false;
  592 + } catch (Exception e) {
  593 + log.warn("检查索引状态失败: {}/{}", i + 1, timeoutSeconds);
  594 + }
  595 + }
  596 +
  597 + log.error("等待索引就绪超时");
  598 + return false;
  599 + }
  600 +
  601 + /**
  602 + * 等待集合加载完成
  603 + */
  604 + private boolean waitForLoad(String collectionName, int timeoutSeconds) {
  605 + log.info("等待集合加载: {},超时: {}秒", collectionName, timeoutSeconds);
  606 +
  607 + for (int i = 0; i < timeoutSeconds; i++) {
  608 + try {
  609 + GetLoadStateReq loadStateReq = GetLoadStateReq.builder()
  610 + .collectionName(collectionName)
  611 + .build();
  612 +
  613 + boolean isLoaded = milvusClient.getLoadState(loadStateReq);
  614 +
  615 + if (isLoaded) {
  616 + log.info("✅ 集合加载完成");
  617 + return true;
  618 + }
  619 +
  620 + Thread.sleep(1000);
  621 +
  622 + } catch (InterruptedException e) {
  623 + Thread.currentThread().interrupt();
  624 + return false;
  625 + } catch (Exception e) {
  626 + log.warn("检查加载状态失败: {}/{}", i + 1, timeoutSeconds);
  627 + }
  628 + }
  629 +
  630 + log.error("集合加载超时");
  631 + return false;
  632 + }
  633 +
  634 +
  635 + /**
  636 + * 批量创建所有索引(向量索引 + 多个标量索引)
  637 + */
  638 + private void createAllIndexes(String collectionName) {
  639 + log.info("开始为集合批量创建索引: {}", collectionName);
  640 +
  641 + // 1. 准备所有索引参数
  642 + List<IndexParam> allIndexParams = new ArrayList<>();
  643 +
  644 + // 1.1 向量索引
  645 + Map<String, Object> vectorExtraParams = new HashMap<>();
  646 + vectorExtraParams.put("nlist", 256); // 聚类中心数:sqrt(384) * 13 ≈ 256
  647 + vectorExtraParams.put("nprobe", 32); // 搜索时检查的聚类数
  648 +
  649 + IndexParam vectorIndex = IndexParam.builder()
  650 + .fieldName("vector")
  651 + .indexName("idx_vector_rebuild")
  652 + .indexType(IndexParam.IndexType.IVF_FLAT)
  653 + .metricType(IndexParam.MetricType.IP)
  654 + .extraParams(vectorExtraParams)
  655 + .build();
  656 + allIndexParams.add(vectorIndex);
  657 +
  658 + // 1.2 create_time 字段索引(用于时间范围查询)
  659 + IndexParam timeIndex = IndexParam.builder()
  660 + .fieldName("create_time")
  661 + .indexName("idx_create_time")
  662 + .indexType(IndexParam.IndexType.STL_SORT) // 排序索引
  663 + .build();
  664 + allIndexParams.add(timeIndex);
  665 +
  666 + // 1.3 question 字段倒排索引(用于文本过滤)
  667 + IndexParam questionIndex = IndexParam.builder()
  668 + .fieldName("sQuestion")
  669 + .indexName("idx_question")
  670 + .indexType(IndexParam.IndexType.INVERTED) // 倒排索引
  671 + .build();
  672 + allIndexParams.add(questionIndex);
  673 +
  674 + // 1.4 data_id 字段索引(用于精确匹配)
  675 + IndexParam idIndex = IndexParam.builder()
  676 + .fieldName("data_id")
  677 + .indexName("idx_data_id")
  678 + .indexType(IndexParam.IndexType.INVERTED)
  679 + .build();
  680 + allIndexParams.add(idIndex);
  681 +
  682 + IndexParam sKey = IndexParam.builder()
  683 + .fieldName("sKey")
  684 + .indexName("s_key")
  685 + .indexType(IndexParam.IndexType.INVERTED)
  686 + .build();
  687 + allIndexParams.add(sKey);
  688 +
  689 + // 1.5 sql_content 字段索引(如果需要)
  690 +// IndexParam sqlIndex = IndexParam.builder()
  691 +// .fieldName("sql_content")
  692 +// .indexName("idx_sql_content")
  693 +// .indexType(IndexParam.IndexType.INVERTED)
  694 +// .build();
  695 +// allIndexParams.add(sqlIndex);
  696 +
  697 + // 2. 批量创建索引
  698 + try {
  699 + CreateIndexReq createIndexReq = CreateIndexReq.builder()
  700 + .collectionName(collectionName)
  701 + .indexParams(allIndexParams) // 一次性传入所有索引
  702 + .sync(true) // 同步等待
  703 + .timeout(120000L) // 总超时时间120秒
  704 + .build();
  705 +
  706 + milvusClient.createIndex(createIndexReq);
  707 + log.info("所有索引批量创建成功: {}", collectionName);
  708 +
  709 + } catch (Exception e) {
  710 + log.error("批量创建索引失败: {}", e.getMessage());
  711 + createScalarIndexes(collectionName);
  712 + }
  713 + }
  714 +
  715 +
  716 +}
... ...
src/main/java/com/xly/milvus/service/impl/EmbeddingServiceImpl.java 0 → 100644
  1 +package com.xly.milvus.service.impl;
  2 +
  3 +import com.xly.milvus.service.EmbeddingService;
  4 +import dev.langchain4j.data.embedding.Embedding;
  5 +import dev.langchain4j.data.segment.TextSegment;
  6 +import dev.langchain4j.model.embedding.EmbeddingModel;
  7 +import lombok.RequiredArgsConstructor;
  8 +import lombok.extern.slf4j.Slf4j;
  9 +import org.springframework.stereotype.Service;
  10 +
  11 +import java.util.ArrayList;
  12 +import java.util.Collections;
  13 +import java.util.List;
  14 +import java.util.stream.Collectors;
  15 +
  16 +@Slf4j
  17 +@Service("embeddingService")
  18 +@RequiredArgsConstructor
  19 +public class EmbeddingServiceImpl implements EmbeddingService {
  20 +
  21 + private final EmbeddingModel embeddingModel;
  22 +
  23 + /**
  24 + * 生成单个文本的向量
  25 + */
  26 + public List<Float> generateEmbedding(String text) {
  27 + if (text == null || text.trim().isEmpty()) {
  28 + log.warn("Input text is empty");
  29 + return null;
  30 + }
  31 +
  32 + try {
  33 + // 0.35.0 API: embed 方法返回 Response<Embedding>
  34 + Embedding embedding = embeddingModel.embed(text).content();
  35 +
  36 + // 将 float[] 转换为 List<Float> 供 Milvus 使用
  37 + float[] vectorArray = embedding.vector();
  38 + List<Float> vectorList = new ArrayList<>(vectorArray.length);
  39 + for (float v : vectorArray) {
  40 + vectorList.add(v);
  41 + }
  42 + return vectorList;
  43 + } catch (Exception e) {
  44 + log.error("Error generating embedding for text: {}", text, e);
  45 + return null;
  46 + }
  47 + }
  48 +
  49 + /**
  50 + * 批量生成向量(高效版)
  51 + * 利用 LangChain4j 内置的并行化能力,显著提升性能
  52 + */
  53 + public List<List<Float>> generateEmbeddings(List<String> texts) {
  54 + if (texts == null || texts.isEmpty()) {
  55 + return Collections.emptyList();
  56 + }
  57 +
  58 + try {
  59 + // 将文本转换为 TextSegment 列表
  60 + List<TextSegment> segments = texts.stream()
  61 + .map(TextSegment::from)
  62 + .collect(Collectors.toList());
  63 +
  64 + // 批量嵌入,内部自动并行化
  65 + List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
  66 +
  67 + // 转换格式
  68 + return embeddings.stream()
  69 + .map(embedding -> {
  70 + float[] array = embedding.vector();
  71 + List<Float> list = new ArrayList<>(array.length);
  72 + for (float v : array) {
  73 + list.add(v);
  74 + }
  75 + return list;
  76 + })
  77 + .collect(Collectors.toList());
  78 +
  79 + } catch (Exception e) {
  80 + log.error("Error generating embeddings in batch", e);
  81 + return Collections.emptyList();
  82 + }
  83 + }
  84 +}
0 85 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java 0 → 100644
  1 +package com.xly.milvus.service.impl;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.collection.ConcurrentHashSet;
  5 +import cn.hutool.core.date.DateUtil;
  6 +import cn.hutool.core.thread.ThreadUtil;
  7 +import cn.hutool.core.util.ObjectUtil;
  8 +import cn.hutool.core.util.StrUtil;
  9 +import com.google.gson.JsonArray;
  10 +import com.google.gson.JsonObject;
  11 +import com.xly.milvus.service.MilvusService;
  12 +import com.xly.milvus.service.VectorizationService;
  13 +import com.xly.milvus.util.MapToJsonConverter;
  14 +import com.xly.service.DynamicExeDbService;
  15 +import com.xly.tts.bean.TTSResponseDTO;
  16 +import io.milvus.response.SearchResultsWrapper;
  17 +import io.milvus.v2.client.MilvusClientV2;
  18 +import io.milvus.v2.common.ConsistencyLevel;
  19 +import io.milvus.v2.common.DataType;
  20 +import io.milvus.v2.common.IndexParam;
  21 +import io.milvus.v2.service.collection.request.*;
  22 +import io.milvus.v2.service.collection.response.DescribeCollectionResp;
  23 +import io.milvus.v2.service.vector.request.DeleteReq;
  24 +import io.milvus.v2.service.vector.request.InsertReq;
  25 +import io.milvus.v2.service.vector.request.SearchReq;
  26 +import io.milvus.v2.service.vector.request.data.FloatVec;
  27 +import io.milvus.v2.service.vector.response.InsertResp;
  28 +import io.milvus.v2.service.vector.response.QueryResp;
  29 +import io.milvus.v2.service.vector.response.SearchResp;
  30 +import lombok.RequiredArgsConstructor;
  31 +import lombok.extern.slf4j.Slf4j;
  32 +import org.springframework.beans.factory.annotation.Value;
  33 +import org.springframework.stereotype.Service;
  34 +
  35 +import java.math.BigDecimal;
  36 +import java.util.*;
  37 +import java.util.regex.Matcher;
  38 +import java.util.regex.Pattern;
  39 +import java.util.stream.Collectors;
  40 +
  41 +/**
  42 + * 向量化服务实现 - 使用LangChain4j的All-MiniLM-L6-v2模型
  43 + */
  44 +@Slf4j
  45 +@Service(value = "milvusService")
  46 +@RequiredArgsConstructor
  47 +public class MilvusServiceImpl implements MilvusService {
  48 +
  49 + private final MilvusClientV2 milvusClient;
  50 + private final VectorizationService vectorizationService;
  51 + private final DynamicExeDbService dynamicExeDbService;
  52 +
  53 + // 或者从配置文件读取
  54 + @Value("${milvus.vector.dimension:384}")
  55 + private int VECTOR_DIM;
  56 +
  57 + // 缓存已经初始化过的 Milvus 集合(线程安全)
  58 + private final Set<String> loadedCollections = new ConcurrentHashSet<>();
  59 +
  60 +
  61 +
  62 + /***
  63 + * @Author 钱豹
  64 + * @Date 22:18 2026/3/24
  65 + * @Param [reqMap]
  66 + * @return void
  67 + * @Description 初始化结构以及数据
  68 + **/
  69 + @Override
  70 + public TTSResponseDTO initDataToMilvus(Map<String, Object> reqMap) {
  71 + if(ObjectUtil.isEmpty(reqMap.get("sInputTabelName"))){
  72 + return TTSResponseDTO.builder()
  73 + .code(-1)
  74 + .message("输入表名")
  75 + .build();
  76 + }
  77 + if(ObjectUtil.isEmpty(reqMap.get("sVectorfiled"))){
  78 + return TTSResponseDTO.builder()
  79 + .code(-1)
  80 + .message("向量库标量字段")
  81 + .build();
  82 + }
  83 + if(ObjectUtil.isEmpty(reqMap.get("sVectorjson"))){
  84 + return TTSResponseDTO.builder()
  85 + .code(-1)
  86 + .message("向量化内容JSON")
  87 + .build();
  88 + }
  89 +
  90 + String sInputTabelName = reqMap.get("sInputTabelName").toString();
  91 + String sVectorfiled = reqMap.get("sVectorfiled").toString();
  92 + String sVectorjson = reqMap.get("sVectorjson").toString();
  93 +
  94 + String tUpdateDate = DateUtil.now();
  95 + String tUpdateDateUp = getUpdateDateUp(sInputTabelName);
  96 + //获取需要同步地数据
  97 + List<Map<String,Object>> data = getAddData(sInputTabelName,tUpdateDate, tUpdateDateUp);
  98 + //创建集合
  99 + createCollectionIfNotExists(sInputTabelName, sVectorfiled, sVectorjson,true);
  100 + if(ObjectUtil.isNotEmpty(data)){
  101 + //插入数据
  102 + long num= addDataToCollection(sInputTabelName, sVectorfiled, sVectorjson,data);
  103 + }
  104 + addAiMilvusVectorRecord(sInputTabelName,tUpdateDate, tUpdateDateUp);
  105 + return TTSResponseDTO.builder()
  106 + .code(200)
  107 + .message("success")
  108 + .build();
  109 + }
  110 +
  111 + public String getUpdateDateUp(String sInputTabelName) {
  112 + Map<String,Object> serDataMap = new HashMap<>();
  113 + String sSql ="SELECT DATE_FORMAT(tUpdateDate,'%Y-%m-%d %H:%i:%s') AS tUpdateDate FROM ai_milvus_vector_record WHERE sInputTabelName = #{sInputTabelName}";
  114 + serDataMap.put("sInputTabelName",sInputTabelName);
  115 + List<Map<String,Object>> data = this.dynamicExeDbService.findSql(serDataMap,sSql);
  116 + if(ObjectUtil.isEmpty(data)){
  117 + return "2000-03-24";
  118 + }
  119 + return data.get(0).get("tUpdateDate").toString();
  120 + }
  121 +
  122 + /***
  123 + * @Author 钱豹
  124 + * @Date 22:24 2026/3/24
  125 + * @Param
  126 + * @return
  127 + * @Description 获取需要同步地数据
  128 + **/
  129 + public List<Map<String,Object>> getAddData(String sInputTabelName,String tUpdateDate,String tUpdateDateUp) {
  130 + //获取需要同步地数据
  131 + Map<String,Object> serDataMap = new HashMap<>();
  132 + serDataMap.put("tUpdateDate",tUpdateDate);
  133 + serDataMap.put("tUpdateDateUp",tUpdateDateUp);
  134 + String sSql = String.format("SELECT * FROM %s WHERE tUpdateDate >= #{tUpdateDateUp} AND tUpdateDate < #{tUpdateDate}",sInputTabelName);
  135 + return this.dynamicExeDbService.findSql(serDataMap,sSql);
  136 + }
  137 +
  138 + /***
  139 + * @Author 钱豹
  140 + * @Date 22:32 2026/3/24
  141 + * @Param [sInputTabelName, tUpdateDate]
  142 + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>>
  143 + * @Description 获取更新地数据
  144 + **/
  145 + public void addAiMilvusVectorRecord(String sInputTabelName,String tUpdateDate,String tUpdateDateUp) {
  146 + //获取需要同步地数据
  147 + delAiMilvusVectorRecord(sInputTabelName);
  148 + Map<String,Object> dMap = new HashMap<>();
  149 + dMap.put("sInputTabelName",sInputTabelName);
  150 + dMap.put("tUpdateDate",tUpdateDate);
  151 + dMap.put("tUpdateDateUp",tUpdateDateUp);
  152 + String sSql = String.format("INSERT INTO ai_milvus_vector_record(sId,sInputTabelName,tUpdateDate,tUpdateDateUp)VALUES(newId(),#{sInputTabelName},#{tUpdateDate},#{tUpdateDateUp})");
  153 + dynamicExeDbService.addSql(dMap,sSql);
  154 + }
  155 +
  156 + /***
  157 + * @Author 钱豹
  158 + * @Date 22:32 2026/3/24
  159 + * @Param [sInputTabelName, tUpdateDate]
  160 + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>>
  161 + * @Description 获取更新地数据
  162 + **/
  163 + public void delAiMilvusVectorRecord(String sInputTabelName) {
  164 + //获取需要同步地数据
  165 + Map<String,Object> dMap = new HashMap<>();
  166 + dMap.put("sInputTabelName",sInputTabelName);
  167 + String sSql = "DELETE FROM ai_milvus_vector_record WHERE sInputTabelName = #{sInputTabelName}";
  168 + dynamicExeDbService.delSql(dMap,sSql);
  169 + }
  170 +
  171 +
  172 + /***
  173 + * @Author 钱豹
  174 + * @Date 16:31 2026/3/24
  175 + * @Param [collectionName]
  176 + * @return void
  177 + * @Description 创建集合
  178 + **/
  179 + @Override
  180 + public void createCollectionIfNotExists(String collectionName,String sVectorfiled,String sVectorjson,Boolean bRset) {
  181 + createCollection(collectionName,sVectorfiled,bRset);
  182 + log.info("集合 {} 创建成功", collectionName);
  183 + // 集合缓存:只加载一次
  184 + if (!loadedCollections.contains(collectionName)) {
  185 + loadedCollections.add(collectionName);
  186 + }
  187 +
  188 + }
  189 +
  190 + /***
  191 + * @Author 钱豹
  192 + * @Date 20:59 2026/3/24
  193 + * @Param [collectionName, sVectorfiled, sVectorjson, bRset]
  194 + * @return void
  195 + * @Description 新增数据集合
  196 + **/
  197 + @Override
  198 + public long addDataToCollection(String collectionName, String sVectorfiled, String sVectorjson,List<Map<String,Object>> data){
  199 +
  200 + // 1. 参数校验(防止空参数导致崩溃)
  201 + if (ObjectUtil.isEmpty(collectionName) || CollUtil.isEmpty(data)) {
  202 + throw new IllegalArgumentException("参数异常:集合名/slaveId/数据不能为空");
  203 + }
  204 +
  205 + // 1. 转换为Milvus格式
  206 + List<JsonObject> rows = convertToMilvusRow(data, sVectorfiled, sVectorjson);
  207 + if (CollUtil.isEmpty(rows)) {
  208 + return 0l; // 无数据直接返回
  209 + }
  210 +
  211 + // 3.先删除再插入
  212 + // 1. 构建删除请求
  213 + // 过滤条件:匹配唯一键
  214 + // 核心:从 data 中提取所有 sSlaveId,批量删除
  215 + List<String> slaveIdList = data.stream()
  216 + .map(map -> map.get("sSlaveId")) // 取每条数据的slaveId
  217 + .filter(Objects::nonNull)
  218 + .map(String::valueOf)
  219 + .distinct() // 去重
  220 + .toList();
  221 +
  222 + if (slaveIdList.isEmpty()) {
  223 + throw new RuntimeException("未获取到slaveId,无法删除旧数据");
  224 + }
  225 +
  226 + // 拼接 Milvus 删除条件:sSlaveId in ['111','222','333']
  227 + String filter = String.format("sSlaveId in [%s]",
  228 + slaveIdList.stream()
  229 + .map(id -> "'" + id + "'")
  230 + .collect(Collectors.joining(","))
  231 + );
  232 +
  233 + // 批量删除
  234 + DeleteReq deleteReq = DeleteReq.builder()
  235 + .collectionName(collectionName)
  236 + .filter(filter)
  237 + .build();
  238 + milvusClient.delete(deleteReq);
  239 +
  240 + // 短暂等待 Milvus 数据同步
  241 + ThreadUtil.sleep(100);
  242 +
  243 + // 4. 插入到Milvus(批量)
  244 + InsertReq insertReq = InsertReq.builder()
  245 + .collectionName(collectionName)
  246 + .data(rows)
  247 + .build();
  248 + InsertResp insertResp = milvusClient.insert(insertReq);
  249 + return insertResp.getInsertCnt();
  250 + }
  251 +
  252 + /***
  253 + * @Author 钱豹
  254 + * @Date 13:29 2026/3/25
  255 + * @Param [sVectorfiled]
  256 + * @return java.util.Map<java.lang.String,java.lang.Object>
  257 + * @Description 返回组装动态内容
  258 + **/
  259 + @Override
  260 + public Map<String,Object> getMilvusFiled(String sVectorfiled){
  261 + String[] sVectorfiledArray = sVectorfiled.split(",");
  262 + List<String> sFileds = new ArrayList<>();
  263 + List<String> sFiledDescriptions = new ArrayList<>();
  264 + List<Map<String,String>> titleList = new LinkedList<>();
  265 + for(String sVectorfiledOne : sVectorfiledArray){
  266 + Map<String,String> title = new HashMap<>();
  267 +
  268 + String[] sVectorfiledOneArray = sVectorfiledOne.split(":");
  269 + String sDescriptions = sVectorfiledOneArray[0];
  270 + String sName = sVectorfiledOneArray[1];
  271 + sFileds.add(sName);
  272 + // 处理描述中可能包含的换行,保持缩进一致
  273 +// String formattedDesc = sDescriptions.replace("\n", "\n ");
  274 +// sFiledDescriptions.add(String.format(" - %s: %s", sName, formattedDesc));
  275 + String formattedDesc =String.format("%s: %s", sName, sDescriptions);
  276 + sFiledDescriptions.add(formattedDesc);
  277 + title.put("sName",sName);
  278 + title.put("sTitle",sDescriptions);
  279 + titleList.add(title);
  280 + }
  281 + Map<String,Object> rMap = new HashMap<>();
  282 + rMap.put("sMilvusFiled", String.join(",", sFileds));
  283 + rMap.put("sMilvusFiledDescription", String.join(",", sFiledDescriptions));
  284 + rMap.put("sFileds", sFileds);
  285 + rMap.put("title", titleList);
  286 + return rMap;
  287 + }
  288 +
  289 + @Override
  290 + public List<Map<String, Object>> getDataToCollection(String collectionName, String milvusFilter,String searchText,Integer size,List<String> fields){
  291 + log.info("开始相似度查询: collection={}, searchText={}", collectionName, searchText);
  292 + // 2. 设置范围搜索参数
  293 + Map<String, Object> searchParams = new HashMap<>();
  294 + searchParams.put("nprobe", 10);
  295 + // 对于 IP 度量,相似度范围在 [minScore, maxScore]
  296 + searchParams.put("radius", 0.9); // 最小相似度
  297 + searchParams.put("range_filter", 1); // 最大相似度
  298 + // 1. 确保集合已加载
  299 +// ensureCollectionLoaded(collectionName);
  300 + // 1. 向量化搜索文本
  301 + List<Float> vectorList = vectorizationService.textToVector(searchText);
  302 + if (vectorList == null || vectorList.isEmpty()) {
  303 + throw new RuntimeException("向量化失败");
  304 + }
  305 + // 2. 转换为 float[]
  306 + float[] floatArray = new float[vectorList.size()];
  307 + for (int i = 0; i < vectorList.size(); i++) {
  308 + floatArray[i] = vectorList.get(i);
  309 + }
  310 +
  311 + // 3. 创建 Milvus FloatVec 对象
  312 + FloatVec floatVec = new FloatVec(floatArray);
  313 + log.info("查询向量库条件{}",milvusFilter);
  314 + milvusFilter = isValidMilvusFilter(milvusFilter)?milvusFilter : null;
  315 + log.info("实际查询向量库条件{}",milvusFilter);
  316 + // 4. 构建搜索请求
  317 + SearchReq searchReq = SearchReq.builder()
  318 + .collectionName(collectionName)
  319 + .data(Collections.singletonList(floatVec))
  320 + .annsField("vector") // 向量字段名
  321 + .topK(size) // 返回最相似的10条
  322 + .metricType(IndexParam.MetricType.IP) // 内积相似度
  323 + .outputFields(fields)
  324 +// .searchParams(searchParams)
  325 + .filter(milvusFilter)
  326 + .build();
  327 + // 5. 执行搜索
  328 + SearchResp searchResp = milvusClient.search(searchReq);
  329 +
  330 + // 6. 处理结果
  331 + return processMilvusResults(searchResp);
  332 + }
  333 +
  334 +
  335 + /**
  336 + * 判断 Milvus 过滤条件是否有效
  337 + * @param milvusFilter 过滤条件字符串
  338 + * @return true: 有效条件, false: 无效条件
  339 + */
  340 + public boolean isValidMilvusFilter(String milvusFilter) {
  341 + // 1. 空值判断
  342 + if (milvusFilter == null || milvusFilter.trim().isEmpty()) {
  343 + return false;
  344 + }
  345 +
  346 + String filter = milvusFilter.trim();
  347 +
  348 + // 2. 基本格式检查:不能是纯布尔值
  349 + if ("true".equalsIgnoreCase(filter) || "false".equalsIgnoreCase(filter)) {
  350 + return false;
  351 + }
  352 +
  353 + // 3. 检查是否包含有效的操作符
  354 + boolean hasValidOperator = filter.matches(".*[=!<>]=?.*") || filter.contains(" like ");
  355 + if (!hasValidOperator) {
  356 + return false;
  357 + }
  358 +
  359 + // 4. 检查字符串值是否使用单引号包裹
  360 + // 匹配模式:字段名 操作符 '值'
  361 + Pattern pattern = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*\\s*(==|!=|>=|<=|>|<|like)\\s*('[^']*'|\\d+)");
  362 + Matcher matcher = pattern.matcher(filter);
  363 +
  364 + // 5. 对于复合条件,递归检查
  365 + if (filter.contains("&&") || filter.contains("||")) {
  366 + // 分割复合条件(简单处理,生产环境需要更完善的解析)
  367 + String[] conditions = filter.split("&&|\\|\\|");
  368 + for (String condition : conditions) {
  369 + condition = condition.trim().replaceAll("^[()]+|[()]+$", ""); // 去除括号
  370 + if (!isValidSimpleCondition(condition)) {
  371 + return false;
  372 + }
  373 + }
  374 + return true;
  375 + }
  376 +
  377 + // 6. 检查简单条件
  378 + return isValidSimpleCondition(filter);
  379 + }
  380 +
  381 + /**
  382 + * 验证简单条件(不包含 && 和 ||)
  383 + */
  384 + private boolean isValidSimpleCondition(String condition) {
  385 + if (condition == null || condition.trim().isEmpty()) {
  386 + return false;
  387 + }
  388 + condition = condition.trim();
  389 + // 匹配简单条件的正则
  390 + // 格式:字段名 操作符 值
  391 + // 字段名:字母开头,包含字母数字下划线
  392 + // 操作符:==, !=, >=, <=, >, <, like
  393 + // 值:单引号字符串 或 数字
  394 + String regex = "^\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*" + // 字段名
  395 + "(==|!=|>=|<=|>|<|like)\\s*" + // 操作符
  396 + "('([^'\\\\]|\\\\.)*'|\\d+(\\.\\d+)?)\\s*$"; // 值
  397 +
  398 + if (!condition.matches(regex)) {
  399 + return false;
  400 + }
  401 + // 额外检查:like 操作符的值必须包含 %
  402 + if (condition.contains(" like ")) {
  403 + String value = condition.split("like")[1].trim();
  404 + if (!value.contains("%")) {
  405 + return false; // like 必须使用 % 通配符
  406 + }
  407 + }
  408 + return true;
  409 + }
  410 +
  411 +
  412 + /**
  413 + * 处理 Milvus 查询结果(完整版)
  414 + */
  415 + /**
  416 + * 处理 Milvus 查询结果(完整版)
  417 + */
  418 + private List<Map<String, Object>> processMilvusResults(SearchResp response) {
  419 + List<Map<String, Object>> results = new ArrayList<>();
  420 + if (response == null) {
  421 + log.warn("Milvus 响应为空");
  422 + return results;
  423 + }
  424 + List<List<SearchResp.SearchResult>> searchResults = response.getSearchResults();
  425 + if (searchResults == null || searchResults.isEmpty()) {
  426 + log.warn("Milvus 搜索结果为空");
  427 + return results;
  428 + }
  429 + // 遍历每个查询的结果集(通常只有一个查询)
  430 + for (List<SearchResp.SearchResult> resultList : searchResults) {
  431 + // 遍历每个搜索结果
  432 + for (SearchResp.SearchResult result : resultList) {
  433 + Map<String, Object> item = new HashMap<>();
  434 + // 获取相似度分数
  435 + Float score = result.getScore();
  436 + if (score != null) {
  437 + item.put("score", score);
  438 + }
  439 + // 获取实体字段数据
  440 + Map<String, Object> entity = result.getEntity();
  441 + // 将所有字段添加到结果中
  442 + item.putAll(entity);
  443 + results.add(item);
  444 + }
  445 + }
  446 + log.info("处理完成,共 {} 条搜索结果", results.size());
  447 + return results;
  448 + }
  449 + /**
  450 + * 从实体对象构建Milvus插入数据
  451 + */
  452 + public List<JsonObject> convertToMilvusRow(List<Map<String,Object>> data, String sVectorfiled,String sVectorjson) {
  453 + List<JsonObject> rows = new ArrayList<>();
  454 + if (CollUtil.isEmpty(data)) {
  455 + return rows;
  456 + }
  457 + // 批量遍历,逐个转换
  458 + for (Map<String, Object> map : data) {
  459 + JsonObject jsonObject = convertToMilvusRowOne(map, sVectorfiled, sVectorjson);
  460 + rows.add(jsonObject);
  461 + }
  462 + return rows;
  463 + }
  464 +
  465 + /***
  466 + * @Author 钱豹
  467 + * @Date 21:31 2026/3/24
  468 + * @Param [data, sVectorfiled, sVectorjson]
  469 + * @return com.google.gson.JsonObject
  470 + * @Description 单个转换
  471 + **/
  472 + public JsonObject convertToMilvusRowOne(Map<String,Object> data, String sVectorfiled,String sVectorjson) {
  473 +
  474 + // ====================== 修复 1:使用真实的向量化文本 ======================
  475 + // 从 sVectorjson 或 data 中获取要向量化的字段值
  476 + StringBuffer vectorText = new StringBuffer();
  477 + getVectorText(data, vectorText, sVectorjson);
  478 + // 向量化
  479 + List<Float> vector = vectorizationService.textToVector(vectorText.toString());
  480 + if (vector == null || vector.isEmpty()) {
  481 + throw new RuntimeException("向量化失败,文本内容:" + vectorText);
  482 + }
  483 + JsonObject row = new JsonObject();
  484 + // 添加向量
  485 + JsonArray vectorArray = new JsonArray();
  486 + vector.forEach(vectorArray::add);
  487 + row.add("vector", vectorArray);
  488 +
  489 + // ====================== 修复 2:sSlaveId 空值安全 ======================
  490 + Object slaveIdObj = data.get("sSlaveId");
  491 + if (slaveIdObj == null) {
  492 + throw new RuntimeException("数据中缺少 sSlaveId 字段,无法插入Milvus");
  493 + }
  494 + row.addProperty("sSlaveId", slaveIdObj.toString());
  495 +
  496 + // 创建时间
  497 + row.addProperty("create_time", System.currentTimeMillis());
  498 +
  499 + // 业务元数据
  500 + JsonObject metadata = MapToJsonConverter.convert(data);
  501 + row.add("metadata", metadata);
  502 +
  503 + // 动态字段
  504 + String[] sVectorfiledArray = sVectorfiled.split(",");
  505 + for (String vectorFieldOne : sVectorfiledArray) {
  506 + String[] fieldArr = vectorFieldOne.split(":");
  507 + if (fieldArr.length < 2) {
  508 + continue;
  509 + }
  510 + String fieldName = fieldArr[1];
  511 + Object value = ObjectUtil.isEmpty(data.get(fieldName)) ? getDefaultData(fieldName) : data.get(fieldName);
  512 + // 通用类型安全添加(你之前的优化方法)
  513 + addToJsonObject(row, fieldName, value);
  514 + }
  515 +
  516 + return row;
  517 + }
  518 +
  519 + private void getVectorText(Map<String,Object> data, StringBuffer vectorText,String sVectorjson){
  520 + // 动态字段
  521 + String[] sVectorjsonArray = sVectorjson.split(";");
  522 + for (String sVectorjsonOne : sVectorjsonArray) {
  523 + String sText;
  524 + String[] fieldArr = sVectorjsonOne.split(":");
  525 + if (fieldArr.length < 2) {
  526 + continue;
  527 + }
  528 + String fieldName = fieldArr[1];
  529 + Object value = ObjectUtil.isEmpty(data.get(fieldName)) ? getDefaultData(fieldName) : data.get(fieldName);
  530 + if (ObjectUtil.isEmpty(value)) {
  531 + sText = StrUtil.EMPTY;
  532 + }else{
  533 + sText = value.toString();
  534 + }
  535 + vectorText.append(" ").append(fieldArr[0]).append(sText);
  536 + }
  537 + }
  538 +
  539 +
  540 + /*******************************************************内部方法********************************************************************************/
  541 +
  542 + /**
  543 + * 安全将 Object 加入 Gson JsonObject,自动识别类型
  544 + */
  545 + private void addToJsonObject(JsonObject row, String fieldName, Object value) {
  546 + if (value == null) {
  547 + row.addProperty(fieldName, "");
  548 + return;
  549 + }
  550 + // 基本类型直接添加
  551 + if (value instanceof String) {
  552 + row.addProperty(fieldName, (String) value);
  553 + } else if (value instanceof Number) {
  554 + row.addProperty(fieldName, (Number) value);
  555 + } else if (value instanceof Boolean) {
  556 + row.addProperty(fieldName, (Boolean) value);
  557 + }
  558 + // List / 数组类型
  559 + else if (value instanceof List<?>) {
  560 + JsonArray jsonArray = new JsonArray();
  561 + for (Object item : (List<?>) value) {
  562 + addJsonElement(jsonArray, item);
  563 + }
  564 + row.add(fieldName, jsonArray);
  565 + }
  566 + // 其他对象转字符串
  567 + else {
  568 + row.addProperty(fieldName, value.toString());
  569 + }
  570 + }
  571 + /**
  572 + * 递归处理 List 元素(支持无限层嵌套 List)
  573 + */
  574 + private void addJsonElement(JsonArray jsonArray, Object item) {
  575 + if (item == null) {
  576 + jsonArray.add((String) null);
  577 + return;
  578 + }
  579 + if (item instanceof String) {
  580 + jsonArray.add((String) item);
  581 + } else if (item instanceof Number) {
  582 + jsonArray.add((Number) item);
  583 + } else if (item instanceof Boolean) {
  584 + jsonArray.add((Boolean) item);
  585 + } else if (item instanceof List<?>) {
  586 + // 递归处理嵌套列表
  587 + JsonArray nestedArray = new JsonArray();
  588 + for (Object nestedItem : (List<?>) item) {
  589 + addJsonElement(nestedArray, nestedItem);
  590 + }
  591 + jsonArray.add(nestedArray);
  592 + } else {
  593 + jsonArray.add(item.toString());
  594 + }
  595 + }
  596 +
  597 + /**
  598 + * 创建集合(定义字段结构)
  599 + */
  600 + private void createCollection(String collectionName,String sVectorfiled,Boolean bRset) {
  601 + // 6. 重新加载集合
  602 + LoadCollectionReq loadReq = LoadCollectionReq.builder()
  603 + .collectionName(collectionName)
  604 + .build();
  605 +
  606 + //是否删除集合 重新创建
  607 + if (bRset){
  608 + // 1. 删除旧集合
  609 + milvusClient.dropCollection(DropCollectionReq.builder()
  610 + .collectionName(collectionName)
  611 + .build());
  612 + //删除对应的记录表
  613 + delAiMilvusVectorRecord(collectionName);
  614 + }
  615 + // 检查集合是否存在
  616 + HasCollectionReq hasCollectionReq = HasCollectionReq.builder()
  617 + .collectionName(collectionName)
  618 + .build();
  619 + boolean exists = milvusClient.hasCollection(hasCollectionReq);
  620 + if (exists) {
  621 + return;
  622 + }
  623 + // 定义字段列表
  624 + List<CreateCollectionReq.FieldSchema> fieldSchemas = new ArrayList<>();
  625 + // 1. 准备所有索引参数
  626 + List<IndexParam> allIndexParams = new ArrayList<>();
  627 + //定义字段列表
  628 + // 1. 主键字段
  629 + fieldSchemas.add( CreateCollectionReq.FieldSchema.builder()
  630 + .name("id")
  631 + .dataType(DataType.Int64)
  632 + .isPrimaryKey(true)
  633 + .autoID(true) // 使用自动ID
  634 + .description("主键ID")
  635 + .build());
  636 + // 3. 主键字段
  637 + fieldSchemas.add(CreateCollectionReq.FieldSchema.builder()
  638 + .name("sSlaveId")
  639 + .dataType(DataType.VarChar)
  640 +// .isPrimaryKey(true) //索引创建
  641 + .maxLength(100)
  642 + .description("原始数据主键ID")
  643 + .build());
  644 + // 2. 向量字段
  645 + fieldSchemas.add(CreateCollectionReq.FieldSchema.builder()
  646 + .name("vector")
  647 + .dataType(DataType.FloatVector)
  648 + .dimension(VECTOR_DIM)
  649 + .description("向量字段,用于相似性搜索")
  650 + .build());
  651 +
  652 + // 4. 创建时间字段
  653 + fieldSchemas.add( CreateCollectionReq.FieldSchema.builder()
  654 + .name("create_time")
  655 + .dataType(DataType.Int64)
  656 + .description("创建时间戳")
  657 + .build());
  658 + // 5. 元数据字段(使用JSON类型存储额外数据)
  659 + fieldSchemas.add(CreateCollectionReq.FieldSchema.builder()
  660 + .name("metadata")
  661 + .dataType(DataType.JSON)
  662 + .description("额外元数据")
  663 + .build());
  664 + //动态字段创建
  665 + String[] sVectorfiledArray = sVectorfiled.split(",");
  666 + for(String sVectorfiledOne : sVectorfiledArray){
  667 + String[] sVectorfiledOneArray = sVectorfiledOne.split(":");
  668 + String sName = sVectorfiledOneArray[1];
  669 + String sDescription = sVectorfiledOneArray[0];
  670 + DataType dataType = processField(sVectorfiledOneArray[1]);
  671 + fieldSchemas.add(CreateCollectionReq.FieldSchema.builder()
  672 + .name(sName)
  673 + .dataType(dataType)
  674 + .description(sDescription)
  675 + .isPrimaryKey(false) // 如果不是主键
  676 + .isNullable(true) // 允许为空
  677 +// .defaultValue("") // 如果有默认值
  678 + // SQL可能较长
  679 + .maxLength(1000)
  680 + .build());
  681 + }
  682 + //创建索引
  683 + createAllIndexes(sVectorfiled,allIndexParams);
  684 +
  685 + // 创建集合schema
  686 + CreateCollectionReq.CollectionSchema schema =
  687 + CreateCollectionReq.CollectionSchema.builder()
  688 + .fieldSchemaList(fieldSchemas)
  689 + .enableDynamicField(true)
  690 + .build();
  691 + // 创建集合请求
  692 + CreateCollectionReq createCollectionReq = CreateCollectionReq.builder()
  693 + .collectionName(collectionName)
  694 + .collectionSchema(schema)
  695 + .indexParams(allIndexParams)//索引集合
  696 + .consistencyLevel(ConsistencyLevel.BOUNDED)
  697 + .build();
  698 +
  699 + // 执行创建集合
  700 + milvusClient.createCollection(createCollectionReq);
  701 + milvusClient.loadCollection(loadReq);
  702 + log.info("集合重新加载成功");
  703 + }
  704 + /**
  705 + * 批量创建所有索引(向量索引 + 多个标量索引)
  706 + */
  707 + private void createAllIndexes(String sVectorfiled,List<IndexParam> allIndexParams) {
  708 +
  709 + // 1.1 向量索引
  710 + Map<String, Object> vectorExtraParams = new HashMap<>(8);
  711 + vectorExtraParams.put("nlist", 256); // 聚类中心数:sqrt(384) * 13 ≈ 256
  712 + vectorExtraParams.put("nprobe", 32); // 搜索时检查的聚类数
  713 +
  714 + IndexParam vectorIndex = IndexParam.builder()
  715 + .fieldName("vector")
  716 + .indexName("idx_vector_rebuild")
  717 + .indexType(IndexParam.IndexType.IVF_FLAT)
  718 + .metricType(IndexParam.MetricType.IP)
  719 + .extraParams(vectorExtraParams)
  720 + .build();
  721 + allIndexParams.add(vectorIndex);
  722 +
  723 + // 1.2 create_time 字段索引(用于时间范围查询)
  724 + IndexParam timeIndex = IndexParam.builder()
  725 + .fieldName("create_time")
  726 + .indexName("idx_create_time")
  727 + .indexType(IndexParam.IndexType.STL_SORT) // 排序索引
  728 + .build();
  729 + allIndexParams.add(timeIndex);
  730 +
  731 + // 1.4 data_id 字段索引(用于精确匹配)
  732 + IndexParam idIndex = IndexParam.builder()
  733 + .fieldName("sSlaveId")
  734 + .indexName("idx_data_id")
  735 + .indexType(IndexParam.IndexType.TRIE)
  736 + .build();
  737 + allIndexParams.add(idIndex);
  738 + //动态字段创建
  739 + String[] sVectorfiledArray = sVectorfiled.split(",");
  740 + for(String sVectorfiledOne : sVectorfiledArray){
  741 + String[] sVectorfiledOneArray = sVectorfiledOne.split(":");
  742 + String sName = sVectorfiledOneArray[1];
  743 + IndexParam.IndexType indexType =indexField(sVectorfiledOneArray[1]);
  744 + allIndexParams.add(IndexParam.builder()
  745 + .fieldName(sName)
  746 + .indexName(sName)
  747 + .indexType(indexType)
  748 + .build());
  749 + }
  750 +
  751 + }
  752 +
  753 + /***
  754 + * @Author 钱豹
  755 + * @Date 21:10 2026/3/24
  756 + * @Param
  757 + * @return
  758 + * @Description //TODO
  759 + **/
  760 + public Object getDefaultData(String sKey) {
  761 + if(sKey.startsWith("d") || sKey.startsWith("i")){
  762 + return BigDecimal.ZERO;
  763 + }else if(sKey.startsWith("b")){
  764 + return false;
  765 + }else{
  766 + return StrUtil.EMPTY;
  767 + }
  768 + }
  769 + /***
  770 + * @Author 钱豹
  771 + * @Date 20:44 2026/3/24
  772 + * @Param [sKey]
  773 + * @return io.milvus.v2.common.DataType
  774 + * @Description 字段类型
  775 + **/
  776 + public DataType processField(String sKey) {
  777 + if(sKey.startsWith("d")){
  778 + return DataType.Double;
  779 + }else if(sKey.startsWith("i")){
  780 + return DataType.Int64;
  781 + }else if(sKey.startsWith("b")){
  782 + return DataType.Bool;
  783 + }else{
  784 + return DataType.VarChar;
  785 + }
  786 + }
  787 +
  788 + /***
  789 + * @Author 钱豹
  790 + * @Date 20:44 2026/3/24
  791 + * @Param [sKey]
  792 + * @return io.milvus.v2.common.DataType
  793 + * @Description 索引类型
  794 + **/
  795 + public IndexParam.IndexType indexField(String sKey) {
  796 + return IndexParam.IndexType.TRIE;
  797 + }
  798 +
  799 +
  800 +
  801 +
  802 +
  803 +}
0 804 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/service/impl/VectorizationServiceImpl.java 0 → 100644
  1 +package com.xly.milvus.service.impl;
  2 +
  3 +import com.xly.milvus.service.VectorizationService;
  4 +import dev.langchain4j.model.embedding.EmbeddingModel;
  5 +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.stereotype.Service;
  8 +
  9 +import java.util.ArrayList;
  10 +import java.util.List;
  11 +import java.util.stream.Collectors;
  12 +
  13 +/**
  14 + * 向量化服务实现 - 使用LangChain4j的All-MiniLM-L6-v2模型
  15 + */
  16 +@Slf4j
  17 +@Service
  18 +public class VectorizationServiceImpl implements VectorizationService {
  19 +
  20 + private final EmbeddingModel embeddingModel;
  21 +
  22 + public VectorizationServiceImpl() {
  23 + // 初始化嵌入模型
  24 + this.embeddingModel = new AllMiniLmL6V2EmbeddingModel();
  25 + log.info("向量化模型初始化成功");
  26 + }
  27 +
  28 + @Override
  29 + public List<Float> textToVector(String text) {
  30 + if (text == null || text.trim().isEmpty()) {
  31 + return new ArrayList<>();
  32 + }
  33 +
  34 + try {
  35 + // 使用LangChain4j生成向量
  36 + dev.langchain4j.data.embedding.Embedding embedding = embeddingModel.embed(text).content();
  37 + float[] vectorArray = embedding.vector();
  38 +
  39 + // 转换为List<Float>
  40 + List<Float> vector = new ArrayList<>();
  41 + for (float v : vectorArray) {
  42 + vector.add(v);
  43 + }
  44 +
  45 + return vector;
  46 + } catch (Exception e) {
  47 + log.error("文本向量化失败: {}", e.getMessage(), e);
  48 + throw new RuntimeException("文本向量化失败", e);
  49 + }
  50 + }
  51 +
  52 + @Override
  53 + public List<List<Float>> batchTextToVector(List<String> texts) {
  54 + if (texts == null || texts.isEmpty()) {
  55 + return new ArrayList<>();
  56 + }
  57 +
  58 + return texts.stream()
  59 + .map(this::textToVector)
  60 + .collect(Collectors.toList());
  61 + }
  62 +}
0 63 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/test/MilvusCompleteTest.java 0 → 100644
  1 +package com.xly.milvus.test;
  2 +
  3 +import com.google.gson.JsonArray;
  4 +import com.google.gson.JsonObject;
  5 +import io.milvus.v2.client.ConnectConfig;
  6 +import io.milvus.v2.client.MilvusClientV2;
  7 +import io.milvus.v2.common.DataType;
  8 +import io.milvus.v2.common.IndexParam;
  9 +import io.milvus.v2.common.ConsistencyLevel;
  10 +import io.milvus.v2.service.collection.request.*;
  11 +import io.milvus.v2.service.collection.response.GetCollectionStatsResp;
  12 +import io.milvus.v2.service.index.request.CreateIndexReq;
  13 +import io.milvus.v2.service.vector.request.InsertReq;
  14 +import io.milvus.v2.service.vector.request.SearchReq;
  15 +import io.milvus.v2.service.vector.request.QueryReq;
  16 +import io.milvus.v2.service.vector.request.data.FloatVec;
  17 +import io.milvus.v2.service.vector.response.InsertResp;
  18 +import io.milvus.v2.service.vector.response.SearchResp;
  19 +import io.milvus.v2.service.vector.response.QueryResp;
  20 +
  21 +import java.util.*;
  22 +import java.util.concurrent.TimeUnit;
  23 +
  24 +/**
  25 + * Milvus Java SDK 2.6.15 完整测试代码
  26 + *
  27 + * 功能:
  28 + * 1. 连接Milvus服务器
  29 + * 2. 创建集合(包含多种字段类型)
  30 + * 3. 创建向量索引
  31 + * 4. 插入测试数据
  32 + * 5. 查询数据(标量过滤)
  33 + * 6. 向量相似性搜索
  34 + * 7. 获取集合统计信息
  35 + * 8. 清理资源
  36 + *
  37 + * @author xly
  38 + * @version 1.0
  39 + */
  40 +public class MilvusCompleteTest {
  41 +
  42 + // ==================== 配置参数 ====================
  43 + private static final String MILVUS_HOST = "121.43.128.225"; // 您的Milvus服务器IP
  44 + private static final int MILVUS_PORT = 19530; // Milvus服务端口
  45 + private static final String COLLECTION_NAME = "complete_test"; // 集合名称
  46 + private static final int VECTOR_DIM = 128; // 向量维度
  47 + private static final int INSERT_COUNT = 15; // 插入数据条数
  48 +
  49 + private MilvusClientV2 client;
  50 +
  51 + public static void main(String[] args) {
  52 + MilvusCompleteTest test = new MilvusCompleteTest();
  53 + try {
  54 + // 执行测试流程
  55 + test.run();
  56 + } catch (Exception e) {
  57 + System.err.println("❌ 测试过程中发生错误: " + e.getMessage());
  58 + e.printStackTrace();
  59 + }
  60 + }
  61 +
  62 + /**
  63 + * 执行完整的测试流程
  64 + */
  65 + private void run() {
  66 + printHeader("Milvus Java SDK 2.6.15 完整测试");
  67 +
  68 + // 1. 连接Milvus
  69 + connect();
  70 +
  71 + try {
  72 + // 2. 清理旧集合
  73 + cleanup();
  74 +
  75 + // 3. 创建集合
  76 + createCollection();
  77 +
  78 + // 4. 创建索引
  79 + createIndex();
  80 +
  81 + // 5. 插入数据
  82 + insertData();
  83 +
  84 + // 6. 查询数据
  85 + queryData();
  86 +
  87 + // 7. 向量搜索
  88 + searchData();
  89 +
  90 + // 8. 获取集合统计
  91 + getCollectionStats();
  92 +
  93 + printSuccess("所有测试完成!");
  94 +
  95 + } catch (Exception e) {
  96 + System.err.println("❌ 测试失败: " + e.getMessage());
  97 + throw e;
  98 + } finally {
  99 + // 9. 清理资源
  100 + releaseAndClose();
  101 + }
  102 + }
  103 +
  104 + /**
  105 + * 连接Milvus服务器
  106 + */
  107 + /**
  108 + * 连接Milvus服务器
  109 + */
  110 + private void connect() {
  111 + printSection("连接Milvus服务器");
  112 +
  113 + try {
  114 + ConnectConfig connectConfig = ConnectConfig.builder()
  115 + .uri("http://" + MILVUS_HOST + ":" + MILVUS_PORT)
  116 + .connectTimeoutMs(TimeUnit.SECONDS.toMillis(30)) // 连接超时30秒
  117 + .keepAliveTimeMs(TimeUnit.MINUTES.toMillis(5)) // 保持连接5分钟
  118 + .keepAliveTimeoutMs(TimeUnit.SECONDS.toMillis(5)) // keep-alive超时5秒
  119 + .keepAliveWithoutCalls(true) // 无调用时保持连接
  120 + .rpcDeadlineMs(TimeUnit.SECONDS.toMillis(10)) // RPC超时10秒
  121 + .enablePrecheck(true) // 启用预检查
  122 + .build();
  123 +
  124 + client = new MilvusClientV2(connectConfig);
  125 +
  126 + // 验证连接
  127 + String serverVersion = client.getServerVersion();
  128 + System.out.println("✅ 连接成功!");
  129 + System.out.println(" - 服务器地址: " + MILVUS_HOST + ":" + MILVUS_PORT);
  130 + System.out.println(" - 服务器版本: " + serverVersion);
  131 +
  132 + } catch (Exception e) {
  133 + System.err.println("❌ 连接失败: " + e.getMessage());
  134 + throw new RuntimeException("无法连接到Milvus服务器", e);
  135 + }
  136 + }
  137 +
  138 + /**
  139 + * 清理已存在的集合
  140 + */
  141 + private void cleanup() {
  142 + printSection("清理环境");
  143 +
  144 + HasCollectionReq hasCollectionReq = HasCollectionReq.builder()
  145 + .collectionName(COLLECTION_NAME)
  146 + .build();
  147 +
  148 + boolean exists = client.hasCollection(hasCollectionReq);
  149 + if (exists) {
  150 + DropCollectionReq dropCollectionReq = DropCollectionReq.builder()
  151 + .collectionName(COLLECTION_NAME)
  152 + .build();
  153 + client.dropCollection(dropCollectionReq);
  154 + System.out.println("✅ 已删除旧集合: " + COLLECTION_NAME);
  155 + } else {
  156 + System.out.println("⏭️ 无需清理,集合不存在");
  157 + }
  158 + }
  159 +
  160 + /**
  161 + * 创建集合
  162 + */
  163 + private void createCollection() {
  164 + printSection("创建集合");
  165 +
  166 + // 定义字段列表
  167 + List<CreateCollectionReq.FieldSchema> fieldSchemas = Arrays.asList(
  168 + // 1. 主键字段
  169 + CreateCollectionReq.FieldSchema.builder()
  170 + .name("id")
  171 + .dataType(DataType.Int64)
  172 + .isPrimaryKey(true)
  173 + .autoID(false)
  174 + .description("主键ID")
  175 + .build(),
  176 +
  177 + // 2. 向量字段
  178 + CreateCollectionReq.FieldSchema.builder()
  179 + .name("vector")
  180 + .dataType(DataType.FloatVector)
  181 + .dimension(VECTOR_DIM)
  182 + .description("向量字段,用于相似性搜索")
  183 + .build(),
  184 +
  185 + // 3. 标题字段
  186 + CreateCollectionReq.FieldSchema.builder()
  187 + .name("title")
  188 + .dataType(DataType.VarChar)
  189 + .maxLength(200)
  190 + .description("标题")
  191 + .build(),
  192 +
  193 + // 4. 内容字段
  194 + CreateCollectionReq.FieldSchema.builder()
  195 + .name("content")
  196 + .dataType(DataType.VarChar)
  197 + .maxLength(1000)
  198 + .description("详细内容")
  199 + .build(),
  200 +
  201 + // 5. 分类字段
  202 + CreateCollectionReq.FieldSchema.builder()
  203 + .name("category")
  204 + .dataType(DataType.VarChar)
  205 + .maxLength(50)
  206 + .description("分类标签")
  207 + .build(),
  208 +
  209 + // 6. 得分字段
  210 + CreateCollectionReq.FieldSchema.builder()
  211 + .name("score")
  212 + .dataType(DataType.Float)
  213 + .description("评分")
  214 + .build(),
  215 +
  216 + // 7. 时间戳字段
  217 + CreateCollectionReq.FieldSchema.builder()
  218 + .name("create_time")
  219 + .dataType(DataType.Int64)
  220 + .description("创建时间戳")
  221 + .build(),
  222 +
  223 + // 8. 标签数组字段
  224 + CreateCollectionReq.FieldSchema.builder()
  225 + .name("tags")
  226 + .dataType(DataType.Array)
  227 + .elementType(DataType.VarChar)
  228 + .maxCapacity(10)
  229 + .description("标签数组")
  230 + .build()
  231 + );
  232 +
  233 + // 创建集合schema
  234 + CreateCollectionReq.CollectionSchema schema =
  235 + CreateCollectionReq.CollectionSchema.builder()
  236 + .fieldSchemaList(fieldSchemas)
  237 + .enableDynamicField(true) // 启用动态字段
  238 + .build();
  239 +
  240 + // 创建集合请求
  241 + CreateCollectionReq createCollectionReq = CreateCollectionReq.builder()
  242 + .collectionName(COLLECTION_NAME)
  243 + .collectionSchema(schema)
  244 + .consistencyLevel(ConsistencyLevel.BOUNDED) // 有界一致性
  245 + .build();
  246 +
  247 + // 执行创建
  248 + client.createCollection(createCollectionReq);
  249 + System.out.println("✅ 集合创建成功: " + COLLECTION_NAME);
  250 + System.out.println(" - 向量维度: " + VECTOR_DIM);
  251 + System.out.println(" - 字段数量: " + fieldSchemas.size());
  252 + }
  253 +
  254 + /**
  255 + * 创建索引
  256 + */
  257 + /**
  258 + * 创建索引
  259 + */
  260 + private void createIndex() {
  261 + printSection("创建索引");
  262 +
  263 + // 创建索引参数
  264 + Map<String, Object> extraParams = new HashMap<>();
  265 + extraParams.put("nlist", 1024);
  266 +
  267 + IndexParam indexParam = IndexParam.builder()
  268 + .fieldName("vector")
  269 + .indexType(IndexParam.IndexType.IVF_FLAT)
  270 + .metricType(IndexParam.MetricType.L2)
  271 + .extraParams(extraParams)
  272 + .build();
  273 +
  274 + // 创建索引请求
  275 + CreateIndexReq createIndexReq = CreateIndexReq.builder()
  276 + .collectionName(COLLECTION_NAME)
  277 + .indexParams(Collections.singletonList(indexParam))
  278 + .build();
  279 +
  280 + // 执行创建
  281 + client.createIndex(createIndexReq);
  282 + System.out.println("✅ 索引创建成功");
  283 + System.out.println(" - 索引类型: IVF_FLAT");
  284 + System.out.println(" - 度量方式: L2 (欧氏距离)");
  285 +
  286 + // 修正:使用 LoadCollectionReq 加载集合到内存
  287 + LoadCollectionReq loadCollectionReq = LoadCollectionReq.builder()
  288 + .collectionName(COLLECTION_NAME)
  289 + .build();
  290 + client.loadCollection(loadCollectionReq);
  291 + System.out.println("✅ 集合已加载到内存");
  292 + }
  293 +
  294 + /**
  295 + * 插入测试数据
  296 + */
  297 + private void insertData() {
  298 + printSection("插入测试数据");
  299 +
  300 + List<JsonObject> rows = new ArrayList<>();
  301 + Random random = new Random(42); // 固定种子,保证可重复性
  302 +
  303 + // 测试数据
  304 + String[] categories = {"科技", "体育", "娱乐", "教育", "财经"};
  305 + String[] titles = {
  306 + "人工智能发展现状与未来趋势", "机器学习入门实战教程",
  307 + "深度学习在图像识别中的应用", "NBA季后赛精彩回顾",
  308 + "世界杯预选赛最新战况", "奥运会筹备工作进展",
  309 + "热门电影推荐排行榜", "音乐榜单TOP10",
  310 + "综艺节目收视率分析", "在线教育行业发展报告",
  311 + "编程语言流行趋势", "高效学习方法分享",
  312 + "股市投资策略分析", "区块链技术应用前景",
  313 + "数字货币市场动态"
  314 + };
  315 +
  316 + long currentTime = System.currentTimeMillis() / 1000;
  317 +
  318 + // 生成测试数据
  319 + for (int i = 1; i <= INSERT_COUNT; i++) {
  320 + JsonObject row = new JsonObject();
  321 +
  322 + // ID
  323 + row.addProperty("id", (long) i);
  324 +
  325 + // 向量
  326 + JsonArray vectorArray = new JsonArray();
  327 + for (int j = 0; j < VECTOR_DIM; j++) {
  328 + vectorArray.add(random.nextFloat());
  329 + }
  330 + row.add("vector", vectorArray);
  331 +
  332 + // 标题
  333 + int index = (i - 1) % titles.length;
  334 + row.addProperty("title", titles[index]);
  335 +
  336 + // 内容
  337 + row.addProperty("content", "这是第" + i + "条测试数据的详细内容。用于测试Milvus的插入和查询功能。");
  338 +
  339 + // 分类
  340 + row.addProperty("category", categories[random.nextInt(categories.length)]);
  341 +
  342 + // 得分
  343 + row.addProperty("score", random.nextFloat() * 100);
  344 +
  345 + // 创建时间
  346 + row.addProperty("create_time", currentTime - random.nextInt(86400) * i);
  347 +
  348 + // 标签数组
  349 + JsonArray tagsArray = new JsonArray();
  350 + tagsArray.add("tag_" + random.nextInt(5));
  351 + tagsArray.add("tag_" + random.nextInt(5));
  352 + row.add("tags", tagsArray);
  353 +
  354 + rows.add(row);
  355 + }
  356 +
  357 + // 批量插入
  358 + InsertReq insertReq = InsertReq.builder()
  359 + .collectionName(COLLECTION_NAME)
  360 + .data(rows)
  361 + .build();
  362 +
  363 + InsertResp insertResp = client.insert(insertReq);
  364 + System.out.println("✅ 成功插入 " + insertResp.getInsertCnt() + " 条数据");
  365 + System.out.println(" - 数据预览:");
  366 +
  367 + // 显示前3条数据的部分信息
  368 + for (int i = 0; i < Math.min(3, rows.size()); i++) {
  369 + JsonObject row = rows.get(i);
  370 + System.out.printf(" [%d] ID: %d, 标题: %s, 分类: %s, 得分: %.2f%n",
  371 + i + 1,
  372 + row.get("id").getAsLong(),
  373 + row.get("title").getAsString(),
  374 + row.get("category").getAsString(),
  375 + row.get("score").getAsFloat());
  376 + }
  377 + }
  378 +
  379 + /**
  380 + * 查询数据(标量过滤)
  381 + */
  382 + private void queryData() {
  383 + printSection("标量查询");
  384 +
  385 + // 查询得分大于50的数据
  386 + QueryReq queryReq = QueryReq.builder()
  387 + .collectionName(COLLECTION_NAME)
  388 + .filter("score > 50") // 过滤条件
  389 + .outputFields(Arrays.asList("id", "title", "category", "score"))
  390 + .limit(10)
  391 + .build();
  392 +
  393 + QueryResp queryResp = client.query(queryReq);
  394 +
  395 + List<QueryResp.QueryResult> results = queryResp.getQueryResults();
  396 + System.out.println("查询条件: score > 50");
  397 + System.out.println("查询结果: " + results.size() + " 条数据");
  398 +
  399 + if (!results.isEmpty()) {
  400 + System.out.println("结果列表:");
  401 + for (QueryResp.QueryResult result : results) {
  402 + Map<String, Object> entity = result.getEntity();
  403 + System.out.printf(" ID: %d | 标题: %s | 分类: %s | 得分: %.2f%n",
  404 + (Long) entity.get("id"),
  405 + entity.get("title"),
  406 + entity.get("category"),
  407 + (Float) entity.get("score"));
  408 + }
  409 + }
  410 + }
  411 +
  412 + /**
  413 + * 向量相似性搜索
  414 + */
  415 + private void searchData() {
  416 + printSection("向量相似性搜索");
  417 +
  418 + // 创建查询向量
  419 + List<Float> vectorList = new ArrayList<>();
  420 + Random random = new Random();
  421 + for (int j = 0; j < VECTOR_DIM; j++) {
  422 + vectorList.add(random.nextFloat());
  423 + }
  424 + FloatVec queryVector = new FloatVec(vectorList);
  425 +
  426 + // 构建搜索参数
  427 + Map<String, Object> searchParams = new HashMap<>();
  428 + searchParams.put("nprobe", 10);
  429 +
  430 + // 构建搜索请求
  431 + SearchReq searchReq = SearchReq.builder()
  432 + .collectionName(COLLECTION_NAME)
  433 + .data(Collections.singletonList(queryVector))
  434 + .annsField("vector")
  435 + .limit(5)
  436 + .outputFields(Arrays.asList("title", "category", "score", "tags"))
  437 + .metricType(IndexParam.MetricType.L2)
  438 + .searchParams(searchParams)
  439 + .build();
  440 +
  441 + // 执行搜索
  442 + SearchResp searchResp = client.search(searchReq);
  443 +
  444 + // 打印结果
  445 + System.out.println("最相似的5条结果:");
  446 + List<List<SearchResp.SearchResult>> results = searchResp.getSearchResults();
  447 +
  448 + if (!results.isEmpty() && !results.get(0).isEmpty()) {
  449 + for (int i = 0; i < results.get(0).size(); i++) {
  450 + SearchResp.SearchResult result = results.get(0).get(i);
  451 + Map<String, Object> entity = result.getEntity();
  452 +
  453 + System.out.printf(" %d. ID: %d | 距离: %.4f%n",
  454 + i + 1,
  455 + (Long) result.getId(),
  456 + result.getScore());
  457 + System.out.printf(" 标题: %s | 分类: %s | 得分: %.2f%n",
  458 + entity.get("title"),
  459 + entity.get("category"),
  460 + (Float) entity.get("score"));
  461 + System.out.printf(" 标签: %s%n",
  462 + entity.get("tags"));
  463 + }
  464 + } else {
  465 + System.out.println(" 没有找到结果");
  466 + }
  467 + }
  468 +
  469 + /**
  470 + * 获取集合统计信息
  471 + */
  472 + private void getCollectionStats() {
  473 + printSection("集合统计信息");
  474 +
  475 + GetCollectionStatsReq statsReq = GetCollectionStatsReq.builder()
  476 + .collectionName(COLLECTION_NAME)
  477 + .build();
  478 +
  479 + GetCollectionStatsResp statsResp = client.getCollectionStats(statsReq);
  480 +
  481 + System.out.println("集合名称: " + COLLECTION_NAME);
  482 + System.out.println("实体数量: " + statsResp.getNumOfEntities());
  483 + }
  484 +
  485 + /**
  486 + * 释放集合并关闭连接
  487 + */
  488 + private void releaseAndClose() {
  489 + printSection("清理资源");
  490 +
  491 + if (client != null) {
  492 + try {
  493 + // 释放集合
  494 + ReleaseCollectionReq releaseCollectionReq = ReleaseCollectionReq.builder()
  495 + .collectionName(COLLECTION_NAME)
  496 + .build();
  497 + client.releaseCollection(releaseCollectionReq);
  498 + System.out.println("✅ 集合已释放");
  499 +
  500 + // 关闭连接
  501 + client.close();
  502 + System.out.println("✅ 连接已关闭");
  503 +
  504 + } catch (Exception e) {
  505 + System.err.println("⚠️ 清理资源时出错: " + e.getMessage());
  506 + }
  507 + }
  508 + }
  509 +
  510 + /**
  511 + * 打印标题
  512 + */
  513 + private void printHeader(String title) {
  514 + System.out.println("\n" + "=".repeat(60));
  515 + System.out.println(" " + title);
  516 + System.out.println("=".repeat(60));
  517 + }
  518 +
  519 + /**
  520 + * 打印章节标题
  521 + */
  522 + private void printSection(String section) {
  523 + System.out.println("\n" + "-".repeat(40));
  524 + System.out.println(" " + section);
  525 + System.out.println("-".repeat(40));
  526 + }
  527 +
  528 + /**
  529 + * 打印成功信息
  530 + */
  531 + private void printSuccess(String message) {
  532 + System.out.println("\n" + "=".repeat(60));
  533 + System.out.println(" ✅ " + message);
  534 + System.out.println("=".repeat(60));
  535 + }
  536 +}
0 537 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/util/MapToJsonConverter.java 0 → 100644
  1 +package com.xly.milvus.util;
  2 +
  3 +import com.google.gson.JsonArray;
  4 +import com.google.gson.JsonObject;
  5 +import java.util.List;
  6 +import java.util.Map;
  7 +
  8 +/**
  9 + * Map 转 JsonObject 工具类
  10 + */
  11 +public class MapToJsonConverter {
  12 +
  13 + /**
  14 + * 将 Map<String, Object> 转换为 JsonObject
  15 + */
  16 + public static JsonObject convert(Map<String, Object> map) {
  17 + JsonObject jsonObject = new JsonObject();
  18 +
  19 + if (map == null || map.isEmpty()) {
  20 + return jsonObject;
  21 + }
  22 +
  23 + for (Map.Entry<String, Object> entry : map.entrySet()) {
  24 + String key = entry.getKey();
  25 + Object value = entry.getValue();
  26 +
  27 + addToJson(jsonObject, key, value);
  28 + }
  29 +
  30 + return jsonObject;
  31 + }
  32 +
  33 + /**
  34 + * 递归添加值到JsonObject
  35 + */
  36 + @SuppressWarnings("unchecked")
  37 + private static void addToJson(JsonObject jsonObject, String key, Object value) {
  38 + if (value == null) {
  39 + jsonObject.add(key, null);
  40 + } else if (value instanceof String) {
  41 + jsonObject.addProperty(key, (String) value);
  42 + } else if (value instanceof Number) {
  43 + jsonObject.addProperty(key, (Number) value);
  44 + } else if (value instanceof Boolean) {
  45 + jsonObject.addProperty(key, (Boolean) value);
  46 + } else if (value instanceof Character) {
  47 + jsonObject.addProperty(key, (Character) value);
  48 + } else if (value instanceof Map) {
  49 + jsonObject.add(key, convert((Map<String, Object>) value));
  50 + } else if (value instanceof List) {
  51 + jsonObject.add(key, convertList((List<?>) value));
  52 + } else if (value instanceof Object[]) {
  53 + jsonObject.add(key, convertArray((Object[]) value));
  54 + } else {
  55 + // 其他类型转换为字符串
  56 + jsonObject.addProperty(key, value.toString());
  57 + }
  58 + }
  59 +
  60 + /**
  61 + * 转换List为JsonArray
  62 + */
  63 + private static JsonArray convertList(List<?> list) {
  64 + JsonArray jsonArray = new JsonArray();
  65 +
  66 + for (Object item : list) {
  67 + if (item == null) {
  68 + jsonArray.add((JsonObject) null);
  69 + } else if (item instanceof String) {
  70 + jsonArray.add((String) item);
  71 + } else if (item instanceof Number) {
  72 + jsonArray.add((Number) item);
  73 + } else if (item instanceof Boolean) {
  74 + jsonArray.add((Boolean) item);
  75 + } else if (item instanceof Character) {
  76 + jsonArray.add((Character) item);
  77 + } else if (item instanceof Map) {
  78 + jsonArray.add(convert((Map<String, Object>) item));
  79 + } else if (item instanceof List) {
  80 + jsonArray.add(convertList((List<?>) item));
  81 + } else {
  82 + jsonArray.add(item.toString());
  83 + }
  84 + }
  85 +
  86 + return jsonArray;
  87 + }
  88 +
  89 + /**
  90 + * 转换数组为JsonArray
  91 + */
  92 + private static JsonArray convertArray(Object[] array) {
  93 + JsonArray jsonArray = new JsonArray();
  94 +
  95 + for (Object item : array) {
  96 + if (item == null) {
  97 + jsonArray.add((JsonObject) null);
  98 + } else if (item instanceof String) {
  99 + jsonArray.add((String) item);
  100 + } else if (item instanceof Number) {
  101 + jsonArray.add((Number) item);
  102 + } else if (item instanceof Boolean) {
  103 + jsonArray.add((Boolean) item);
  104 + } else if (item instanceof Character) {
  105 + jsonArray.add((Character) item);
  106 + } else if (item instanceof Map) {
  107 + jsonArray.add(convert((Map<String, Object>) item));
  108 + } else if (item instanceof List) {
  109 + jsonArray.add(convertList((List<?>) item));
  110 + } else {
  111 + jsonArray.add(item.toString());
  112 + }
  113 + }
  114 +
  115 + return jsonArray;
  116 + }
  117 +}
0 118 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/util/MilvusSchemaBuilder.java 0 → 100644
  1 +package com.xly.milvus.util;
  2 +
  3 +import io.milvus.v2.common.DataType;
  4 +import io.milvus.v2.service.collection.request.CreateCollectionReq;
  5 +import java.util.HashMap;
  6 +import java.util.Map;
  7 +
  8 +/**
  9 + * Milvus字段Schema构建工具类
  10 + * 基于源码的 FieldSchema.FieldSchemaBuilder
  11 + */
  12 +public class MilvusSchemaBuilder {
  13 +
  14 + /**
  15 + * 创建主键ID字段(Int64类型,自增)
  16 + */
  17 + public static CreateCollectionReq.FieldSchema createAutoIdField(String fieldName) {
  18 + return CreateCollectionReq.FieldSchema.builder()
  19 + .name(fieldName)
  20 + .dataType(DataType.Int64)
  21 + .isPrimaryKey(true)
  22 + .autoID(true)
  23 + .description("自增主键ID")
  24 + .build();
  25 + }
  26 +
  27 + /**
  28 + * 创建主键ID字段(String类型,不自增)
  29 + */
  30 + public static CreateCollectionReq.FieldSchema createStringIdField(String fieldName, int maxLength) {
  31 + return CreateCollectionReq.FieldSchema.builder()
  32 + .name(fieldName)
  33 + .dataType(DataType.VarChar)
  34 + .maxLength(maxLength)
  35 + .isPrimaryKey(true)
  36 + .autoID(false)
  37 + .description("业务主键ID")
  38 + .build();
  39 + }
  40 +
  41 + /**
  42 + * 创建向量字段
  43 + */
  44 + public static CreateCollectionReq.FieldSchema createVectorField(String fieldName, int dimension) {
  45 + return CreateCollectionReq.FieldSchema.builder()
  46 + .name(fieldName)
  47 + .dataType(DataType.FloatVector)
  48 + .dimension(dimension)
  49 + .description("向量字段,维度: " + dimension)
  50 + .build();
  51 + }
  52 +
  53 + /**
  54 + * 创建文本字段(VarChar类型)
  55 + */
  56 + public static CreateCollectionReq.FieldSchema createTextField(String fieldName, int maxLength) {
  57 + return CreateCollectionReq.FieldSchema.builder()
  58 + .name(fieldName)
  59 + .dataType(DataType.VarChar)
  60 + .maxLength(maxLength)
  61 + .description("文本内容")
  62 + .build();
  63 + }
  64 +
  65 + /**
  66 + * 创建JSON元数据字段
  67 + */
  68 + public static CreateCollectionReq.FieldSchema createJsonField(String fieldName) {
  69 + return CreateCollectionReq.FieldSchema.builder()
  70 + .name(fieldName)
  71 + .dataType(DataType.JSON)
  72 + .description("JSON元数据")
  73 + .build();
  74 + }
  75 +
  76 + /**
  77 + * 创建整型字段
  78 + */
  79 + public static CreateCollectionReq.FieldSchema createIntField(String fieldName, String description) {
  80 + return CreateCollectionReq.FieldSchema.builder()
  81 + .name(fieldName)
  82 + .dataType(DataType.Int64)
  83 + .description(description)
  84 + .build();
  85 + }
  86 +
  87 + /**
  88 + * 创建整型字段(可空)
  89 + */
  90 + public static CreateCollectionReq.FieldSchema createIntField(String fieldName,
  91 + String description,
  92 + boolean isNullable) {
  93 + return CreateCollectionReq.FieldSchema.builder()
  94 + .name(fieldName)
  95 + .dataType(DataType.Int64)
  96 + .description(description)
  97 + .isNullable(isNullable)
  98 + .build();
  99 + }
  100 +
  101 + /**
  102 + * 创建整型字段(带默认值)
  103 + */
  104 + public static CreateCollectionReq.FieldSchema createIntField(String fieldName,
  105 + String description,
  106 + long defaultValue) {
  107 + return CreateCollectionReq.FieldSchema.builder()
  108 + .name(fieldName)
  109 + .dataType(DataType.Int64)
  110 + .description(description)
  111 + .defaultValue(defaultValue)
  112 + .build();
  113 + }
  114 +
  115 + /**
  116 + * 创建浮点型字段
  117 + */
  118 + public static CreateCollectionReq.FieldSchema createFloatField(String fieldName, String description) {
  119 + return CreateCollectionReq.FieldSchema.builder()
  120 + .name(fieldName)
  121 + .dataType(DataType.Float)
  122 + .description(description)
  123 + .build();
  124 + }
  125 +
  126 + /**
  127 + * 创建布尔型字段
  128 + */
  129 + public static CreateCollectionReq.FieldSchema createBoolField(String fieldName, String description) {
  130 + return CreateCollectionReq.FieldSchema.builder()
  131 + .name(fieldName)
  132 + .dataType(DataType.Bool)
  133 + .description(description)
  134 + .build();
  135 + }
  136 +
  137 + /**
  138 + * 创建布尔型字段(带默认值)
  139 + */
  140 + public static CreateCollectionReq.FieldSchema createBoolField(String fieldName,
  141 + String description,
  142 + boolean defaultValue) {
  143 + return CreateCollectionReq.FieldSchema.builder()
  144 + .name(fieldName)
  145 + .dataType(DataType.Bool)
  146 + .description(description)
  147 + .defaultValue(defaultValue)
  148 + .build();
  149 + }
  150 +
  151 + /**
  152 + * 创建数组字段
  153 + */
  154 + public static CreateCollectionReq.FieldSchema createArrayField(String fieldName,
  155 + DataType elementType,
  156 + int maxCapacity,
  157 + String description) {
  158 + return CreateCollectionReq.FieldSchema.builder()
  159 + .name(fieldName)
  160 + .dataType(DataType.Array)
  161 + .elementType(elementType)
  162 + .maxCapacity(maxCapacity)
  163 + .description(description)
  164 + .build();
  165 + }
  166 +
  167 + /**
  168 + * 创建分区键字段
  169 + */
  170 + public static CreateCollectionReq.FieldSchema createPartitionKeyField(String fieldName,
  171 + DataType dataType,
  172 + String description) {
  173 + return CreateCollectionReq.FieldSchema.builder()
  174 + .name(fieldName)
  175 + .dataType(dataType)
  176 + .isPartitionKey(true)
  177 + .description(description)
  178 + .build();
  179 + }
  180 +
  181 + /**
  182 + * 创建聚类键字段
  183 + */
  184 + public static CreateCollectionReq.FieldSchema createClusteringKeyField(String fieldName,
  185 + DataType dataType,
  186 + String description) {
  187 + return CreateCollectionReq.FieldSchema.builder()
  188 + .name(fieldName)
  189 + .dataType(dataType)
  190 + .isClusteringKey(true)
  191 + .description(description)
  192 + .build();
  193 + }
  194 +
  195 + /**
  196 + * 创建带分析器的文本字段(用于全文搜索)
  197 + */
  198 + public static CreateCollectionReq.FieldSchema createAnalyzedTextField(String fieldName,
  199 + int maxLength,
  200 + String analyzerType) {
  201 + Map<String, Object> analyzerParams = new HashMap<>();
  202 + analyzerParams.put("type", analyzerType); // "english", "chinese", "standard" 等
  203 +
  204 + return CreateCollectionReq.FieldSchema.builder()
  205 + .name(fieldName)
  206 + .dataType(DataType.VarChar)
  207 + .maxLength(maxLength)
  208 + .enableAnalyzer(true)
  209 + .analyzerParams(analyzerParams)
  210 + .enableMatch(true)
  211 + .description("支持全文搜索的文本字段")
  212 + .build();
  213 + }
  214 +}
0 215 \ No newline at end of file
... ...
src/main/java/com/xly/milvus/web/MilvusController.java 0 → 100644
  1 +package com.xly.milvus.web;
  2 +
  3 +import com.xly.milvus.service.MilvusService;
  4 +import com.xly.runner.AppStartupRunner;
  5 +import com.xly.service.DynamicExeDbService;
  6 +import com.xly.service.UserSceneSessionService;
  7 +import com.xly.tool.DynamicToolProvider;
  8 +import com.xly.tts.bean.*;
  9 +import com.xly.tts.service.LocalAudioCache;
  10 +import com.xly.tts.service.PythonTtsProxyService;
  11 +import jakarta.validation.Valid;
  12 +import lombok.RequiredArgsConstructor;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.core.io.InputStreamResource;
  15 +import org.springframework.http.MediaType;
  16 +import org.springframework.http.ResponseEntity;
  17 +import org.springframework.web.bind.annotation.*;
  18 +import reactor.core.publisher.Flux;
  19 +import reactor.core.publisher.Mono;
  20 +
  21 +import javax.annotation.PostConstruct;
  22 +import javax.annotation.PreDestroy;
  23 +import java.util.List;
  24 +import java.util.Map;
  25 +import java.util.concurrent.CompletableFuture;
  26 +
  27 +@Slf4j
  28 +@RestController
  29 +@RequestMapping("/api/milvus")
  30 +@RequiredArgsConstructor
  31 +public class MilvusController {
  32 +
  33 + private final MilvusService milvusService;
  34 +
  35 + /***
  36 + * @Author 钱豹
  37 + * @Date 14:32 2026/2/10
  38 + * @Param [request]
  39 + * @return org.springframework.http.ResponseEntity<com.xly.tts.bean.TTSResponseDTO>
  40 + * @Description 初始化AI所有变量 热启动
  41 + **/
  42 + @PostMapping("/init")
  43 + public ResponseEntity<TTSResponseDTO> init(@RequestBody Map<String,Object> reqMap) {
  44 + TTSResponseDTO responseDTO = milvusService.initDataToMilvus(reqMap);
  45 + return ResponseEntity.ok(responseDTO);
  46 + }
  47 +
  48 +
  49 +}
0 50 \ No newline at end of file
... ...
src/main/java/com/xly/service/XlyErpService.java
... ... @@ -7,6 +7,7 @@ import cn.hutool.core.util.IdUtil;
7 7 import cn.hutool.core.util.ObjectUtil;
8 8 import cn.hutool.core.util.StrUtil;
9 9 import com.alibaba.fastjson2.JSON;
  10 +import com.alibaba.fastjson2.JSONObject;
10 11 import com.xly.agent.ChatiAgent;
11 12 import com.xly.agent.DynamicTableNl2SqlAiAgent;
12 13 import com.xly.agent.ErpAiAgent;
... ... @@ -17,15 +18,14 @@ import com.xly.constant.ReturnTypeCode;
17 18 import com.xly.entity.*;
18 19 import com.xly.exception.sqlexception.SqlGenerateException;
19 20 import com.xly.exception.sqlexception.SqlValidateException;
  21 +import com.xly.milvus.service.AiGlobalAgentQuestionSqlEmitterService;
  22 +import com.xly.milvus.service.MilvusService;
20 23 import com.xly.runner.AppStartupRunner;
21 24 import com.xly.thread.AiSqlErrorHistoryThread;
22 25 import com.xly.thread.AiUserAgentQuestionThread;
23 26 import com.xly.thread.MultiThreadPoolServer;
24 27 import com.xly.tool.DynamicToolProvider;
25   -import com.xly.util.EnhancedErrorGuidance;
26   -import com.xly.util.InputPreprocessor;
27   -import com.xly.util.SqlValidateUtil;
28   -import com.xly.util.ValiDataUtil;
  28 +import com.xly.util.*;
29 29 import dev.langchain4j.agent.tool.ToolExecutionRequest;
30 30 import dev.langchain4j.data.message.AiMessage;
31 31 import dev.langchain4j.data.message.ChatMessage;
... ... @@ -33,15 +33,22 @@ import dev.langchain4j.data.message.ChatMessageType;
33 33 import dev.langchain4j.model.chat.ChatLanguageModel;
34 34 import dev.langchain4j.model.ollama.OllamaChatModel;
35 35 import dev.langchain4j.service.AiServices;
  36 +import dev.langchain4j.service.MemoryId;
  37 +import dev.langchain4j.service.V;
  38 +import io.milvus.v2.common.DataType;
  39 +import io.milvus.v2.service.collection.request.CreateCollectionReq;
36 40 import lombok.RequiredArgsConstructor;
37 41 import lombok.extern.slf4j.Slf4j;
  42 +import org.apache.commons.lang3.time.DateFormatUtils;
38 43 import org.springframework.beans.factory.annotation.Value;
39 44 import org.springframework.stereotype.Service;
40 45 import org.springframework.util.IdGenerator;
  46 +import reactor.core.publisher.Flux;
41 47  
42 48 import java.time.Duration;
43 49 import java.util.*;
44 50 import java.util.stream.Collectors;
  51 +import java.util.stream.IntStream;
45 52  
46 53 @Service
47 54 @RequiredArgsConstructor
... ... @@ -56,6 +63,8 @@ public class XlyErpService {
56 63 private final OperableChatMemoryProvider operableChatMemoryProvider;
57 64 private final DynamicExeDbService dynamicExeDbService;
58 65 private final RedisService redisService;
  66 + private final AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService;
  67 + private final MilvusService milvusService;
59 68  
60 69 //执行动态语句 执行异常的情况下 最多执行次数
61 70 private final Integer maxRetries = 5;
... ... @@ -67,6 +76,51 @@ public class XlyErpService {
67 76  
68 77 @Value("${langchain4j.ollama.sql-model-name}")
69 78 private String sqlModelName;
  79 +
  80 +
  81 + /**
  82 + * 新的流式方法 - 返回 Flux<AiResponseDTO>
  83 + * 每个AiResponseDTO包含一个文本片段
  84 + */
  85 + /**
  86 + * 模拟的erpUserInputStream实现
  87 + */
  88 + public Flux<AiResponseDTO> erpUserInputStream(String userInput, String sUserId,
  89 + String sUserName, String sBrandsId,
  90 + String sSubsidiaryId, String sUserType,
  91 + String authorization) {
  92 + String requestId = UUID.randomUUID().toString();
  93 +
  94 + // 按句子分割
  95 + String[] sentences = userInput.split("(?<=[。!?.!?])");
  96 + int totalChunks = sentences.length;
  97 +
  98 + return Flux.range(0, totalChunks)
  99 + .delayElements(Duration.ofMillis(200))
  100 + .map(i -> {
  101 + String sentence = sentences[i].trim();
  102 + if (sentence.isEmpty()) return null;
  103 +
  104 + return AiResponseDTO.builder()
  105 + .requestId(requestId)
  106 + .code(200)
  107 + .message("ERP_CHUNK")
  108 + .status("PROCESSING")
  109 + .textFragment(sentence)
  110 + .chunkIndex(i)
  111 + .totalChunks(totalChunks)
  112 + .isLastChunk(i == totalChunks - 1)
  113 + .progress((i + 1) * 100 / totalChunks)
  114 + .timestamp(System.currentTimeMillis())
  115 + .sSceneName("客服咨询")
  116 + .sMethodName("chat")
  117 + .sReturnType("MARKDOWN")
  118 + .build();
  119 + })
  120 + .filter(Objects::nonNull);
  121 + }
  122 +
  123 +
70 124 /***
71 125 * @Author 钱豹
72 126 * @Date 19:18 2026/1/27
... ... @@ -116,8 +170,15 @@ public class XlyErpService {
116 170 if (aiAgent == null){
117 171 return getChatiAgent (input,session);
118 172 }
119   - //用户输入添加方法
120   - String sResponMessage = aiAgent.chat(userId, input);
  173 + String sResponMessage = StrUtil.EMPTY;
  174 + //用户输入添加方法(如果没有方法,动态SQL方法不需要)
  175 + if(!(ObjectUtil.isNotEmpty(session.getCurrentTool())
  176 + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName())
  177 + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo()))
  178 + ){
  179 + sResponMessage = aiAgent.chat(userId, input);
  180 + }
  181 +
121 182 if(ObjectUtil.isNotEmpty(session.getCurrentTool())
122 183 && !ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName())
123 184 ){
... ... @@ -132,7 +193,14 @@ public class XlyErpService {
132 193 && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName())
133 194 && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo()))
134 195 ){
135   - sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY,"0",StrUtil.EMPTY, aiAgent);
  196 + //查询是否走向量库 还是数据库查询
  197 + Boolean isAggregation = aiAgent.routeQuery(session.getUserId(), input);
  198 + if(!isAggregation){
  199 + //获取常量库内容
  200 + sResponMessage = getMilvus(session, input, aiAgent);
  201 + }else {
  202 + sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY,"0",StrUtil.EMPTY, aiAgent);
  203 + }
136 204 return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(sResponMessage).sReturnType(ReturnTypeCode.HTML.getCode()).build();
137 205 } else if (ObjectUtil.isNotEmpty(session.getCurrentTool())) {
138 206 //2.处理工具参数采集结束后业务逻辑处理
... ... @@ -198,6 +266,75 @@ public class XlyErpService {
198 266 return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(StrUtil.EMPTY).aiText(StrUtil.EMPTY).systemText("清除记忆成功!").sReturnType(ReturnTypeCode.HTML.getCode()).build();
199 267 }
200 268  
  269 + /***
  270 + * @Author 钱豹
  271 + * @Date 10:16 2026/3/25
  272 + * @Param [session, input, userId, userInput, attempt, errorSql, errorMessage, iErroCount, historySqlList, aiAgent]
  273 + * @return java.lang.String
  274 + * @Description 查询向量库
  275 + **/
  276 + private String getMilvus(UserSceneSession session,String userInput,ErpAiAgent aiAgent){
  277 + String resultExplain = "信息模糊,请提供更具体的问题或指令";
  278 + try{
  279 + String sVectorfiled = session.getCurrentTool().getSVectorfiled();
  280 + String sInputTabelName = session.getCurrentTool().getSInputTabelName();
  281 + Map<String,Object> rMap = milvusService.getMilvusFiled(sVectorfiled);
  282 + String sMilvusFiled = rMap.get("sMilvusFiled").toString();
  283 + String sMilvusFiledDescription = rMap.get("sMilvusFiledDescription").toString();
  284 + List<String> fields = (List<String>) rMap.get("sFileds");
  285 +// List<Map<String, String>> title = (List<Map<String, String>>) rMap.get("title");
  286 + String milvusFilter = aiAgent.getMilvusFilter(session.getUserId(),userInput, sMilvusFiled, sMilvusFiledDescription);
  287 + List<Map<String,Object>> data = milvusService.getDataToCollection(sInputTabelName, milvusFilter,userInput,100,fields);
  288 + //采用表格形式显示
  289 + resultExplain = aiAgent.explainMilvusResult(session.getUserId(),userInput,sMilvusFiledDescription,JSONObject.toJSONString(data));
  290 + //buildMarkdownTableWithStream(data, title);
  291 + return resultExplain;
  292 + }catch (Exception e){
  293 + e.printStackTrace();
  294 + }
  295 + return resultExplain;
  296 + }
  297 + /***
  298 + * @Author 钱豹
  299 + * @Date 13:19 2026/3/25
  300 + * @Param [data, title]
  301 + * @return java.lang.String
  302 + * @Description 数据转成MarkdownTable
  303 + **/
  304 + public String buildMarkdownTableWithStream(List<Map<String, Object>> data, List<Map<String, String>> title) {
  305 + if (data == null || data.isEmpty()) {
  306 + return "暂无数据";
  307 + }
  308 +
  309 + // 动态构建表头
  310 + StringBuilder headerBuilder = new StringBuilder("|");
  311 + StringBuilder separatorBuilder = new StringBuilder("|");
  312 +
  313 + for (Map<String, String> column : title) {
  314 + String displayName = column.get("sTitle"); // 中文显示名称
  315 + headerBuilder.append(" ").append(displayName).append(" |");
  316 + separatorBuilder.append("------|");
  317 + }
  318 + String header = headerBuilder.toString() + "\n" + separatorBuilder.toString() + "\n";
  319 + // 构建数据行
  320 + String rows = IntStream.range(0, data.size())
  321 + .mapToObj(i -> {
  322 + Map<String, Object> item = data.get(i);
  323 + StringBuilder rowBuilder = new StringBuilder("|");
  324 +
  325 + for (Map<String, String> column : title) {
  326 + String fieldName = column.get("sName");
  327 + Object value = item.getOrDefault(fieldName, "");
  328 + rowBuilder.append(" ").append(value).append(" |");
  329 + }
  330 +
  331 + return rowBuilder.toString();
  332 + })
  333 + .collect(Collectors.joining("\n"));
  334 +
  335 + return header + rows;
  336 + }
  337 +
201 338  
202 339 /***
203 340 * @Author 钱豹
... ... @@ -283,48 +420,54 @@ public class XlyErpService {
283 420 **/
284 421 private String getDynamicTableSqlExec(UserSceneSession session,String input,String userId,String userInput,String errorSql,String errorMessage,String iErroCount,String historySqlList,ErpAiAgent aiAgent){
285 422 // 1. 构建自然语言转SQLAgent,
286   - List<Map<String, Object>> sqlResult = new ArrayList<>();
  423 + List<Map<String, Object>> sqlResult;
287 424 String cleanSql = StrUtil.EMPTY;
288 425 String rawSql;
289   - String tableStruct;
290   - String sError_mes = StrUtil.EMPTY;
291   - Boolean doSqlErro = false;
292   - List<ChatMessage> chatMessage = operableChatMemoryProvider.getCurrentChatMessages(session.getUserId());
  426 + String tableStruct = session.getCurrentTool().getSStructureMemo();
  427 + String sError_mes;
  428 + Boolean doAddSql = false;
  429 + List<ChatMessage> chatMessage = new ArrayList<>();
293 430 try{
294   - DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session);
295   - String tableNames = session.getCurrentTool().getSInputTabelName();
296   - // "订单表:viw_salsalesorder,客户信息表:elecustomer,结算方式表:sispayment,产品表(无单价,无金额,无数量):viw_product_sort,销售人员表:viw_sissalesman_depart";
297   - tableStruct = session.getCurrentTool().getSStructureMemo();
298   - String sDataNow = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_FORMAT);
299   -
300   - if(ObjectUtil.isEmpty(errorSql) && ObjectUtil.isEmpty(errorMessage)){
301   - rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput);
302   - }else{
303   - rawSql = aiDynamicTableNl2SqlAiAgent.regenerateSqlWithError(userId, tableNames,tableStruct,sDataNow,userInput,errorSql,errorMessage,iErroCount,historySqlList);
304   - }
305   - log.info("rawSql:"+rawSql);
306   - if (rawSql == null || rawSql.trim().isEmpty()) {
307   - throw new SqlValidateException("SQL EMPTY");
  431 + //获取缓存动态SQL
  432 +// cleanSql = getDynamicTableNl2Sql(session,input);
  433 + //如果之前已查询直接返回
  434 + if(ObjectUtil.isEmpty(cleanSql)){
  435 + DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session);
  436 + chatMessage = operableChatMemoryProvider.getCurrentChatMessages(session.getUserId());
  437 + String tableNames = session.getCurrentTool().getSInputTabelName();
  438 + // "订单表:viw_salsalesorder,客户信息表:elecustomer,结算方式表:sispayment,产品表(无单价,无金额,无数量):viw_product_sort,销售人员表:viw_sissalesman_depart";
  439 + String sDataNow = DateUtil.now();
  440 + //DateFormatUtils.format(new Date(), "yyyy年MM月dd日HH时mm分ss秒");
  441 +// String sDataNow = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_FORMAT);
  442 +
  443 + if(ObjectUtil.isEmpty(errorSql) && ObjectUtil.isEmpty(errorMessage)){
  444 + rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput);
  445 + }else{
  446 + rawSql = aiDynamicTableNl2SqlAiAgent.regenerateSqlWithError(userId, tableNames,tableStruct,sDataNow,userInput,errorSql,errorMessage,iErroCount,historySqlList);
  447 + }
  448 + log.info("rawSql:"+rawSql);
  449 + if (rawSql == null || rawSql.trim().isEmpty()) {
  450 + throw new SqlValidateException("SQL EMPTY");
  451 + }
  452 + // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略)
  453 + cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql);
  454 + SqlValidateUtil.validateMysqlSql(cleanSql);
  455 + doAddSql = true;
308 456 }
309   - // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略)
310   - cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql);
311   - // String[] cleanSqlA = rawSql.split(";");
312   - // if(cleanSqlA.length>1){
313   - // cleanSql = cleanSqlA[cleanSqlA.length-1];
314   - // }
315   - SqlValidateUtil.validateMysqlSql(cleanSql);
316   - // 4. 执行SQL获取结构化结果
317   - // Map<String,Object> params = new HashMap<>();
  457 +// List<ChatMessage> chatMessage2 = operableChatMemoryProvider.getCurrentChatMessages(session.getUserId());
318 458 try{
319 459 sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql);
320 460 }catch (Exception e){
321 461 throw new SqlGenerateException(e.getMessage()+" OLDSQL "+cleanSql);
322 462 }
323 463 }catch (SqlValidateException e){
  464 + //删除记录
  465 +// operableChatMemoryProvider.deleteUserLasterMessageBySize(userId,3);
324 466 sError_mes = e.getMessage();
325 467 doAiSqlErrorHistoryThread(session, StrUtil.EMPTY, cleanSql, sError_mes,input);
326 468 throw e;
327 469 }catch (SqlGenerateException e){
  470 +// operableChatMemoryProvider.deleteUserLasterMessageBySize(userId,3);
328 471 sError_mes = e.getMessage();
329 472 doAiSqlErrorHistoryThread(session, StrUtil.EMPTY, cleanSql, sError_mes,input);
330 473 throw e;
... ... @@ -337,9 +480,11 @@ public class XlyErpService {
337 480 if(Integer.valueOf(iErroCount)>0){
338 481 doAiSqlErrorHistoryThread(session, cleanSql, StrUtil.EMPTY, StrUtil.EMPTY,input);
339 482 }
340   -
341   - //执行操作记录表
342   - doAiUserAgentQuestion(session,input,cleanSql,chatMessage);
  483 + //插入常用操作
  484 + if(doAddSql){
  485 + //执行操作记录表
  486 + doAiUserAgentQuestion(session,input,cleanSql,chatMessage);
  487 + }
343 488 String sText = aiAgent.explainSqlResult(
344 489 userId,
345 490 userInput,
... ... @@ -350,6 +495,28 @@ public class XlyErpService {
350 495 return sText;
351 496 }
352 497  
  498 + /***
  499 + * @Author 钱豹
  500 + * @Date 17:04 2026/3/19
  501 + * @Param [session]
  502 + * @return java.lang.String
  503 + * @Description 获取动态SQL(历史中查询)
  504 + **/
  505 + private String getDynamicTableNl2Sql(UserSceneSession session,String input){
  506 +// String sReidKey = SqlValidateUtil.getsKey( session.getCurrentScene().getSId(), session.getCurrentTool().getSId(), input);
  507 +// Object sSql = redisService.get(sReidKey);
  508 +// if(ObjectUtil.isNotEmpty(sSql)){
  509 +// return sSql.toString();
  510 +// }
  511 + String searchText = session.getCurrentScene().getSId()+"_"+session.getCurrentTool().getSId()+input;
  512 + //SqlValidateUtil.getsKey( session.getCurrentScene().getSId(), session.getCurrentTool().getSId(), SqlValidateUtil.getsQuestion(session.getSUserQuestionList()));
  513 + //根据问题查询向量库
  514 + Map<String,Object> serMap = aiGlobalAgentQuestionSqlEmitterService.queryAiGlobalAgentQuestionSqlEmitter(searchText, "ai_global_agent_question_sql");
  515 + if(ObjectUtil.isNotEmpty(serMap)){
  516 + return serMap.get("sSqlContent").toString();
  517 + }
  518 + return null;
  519 + }
353 520  
354 521 /***
355 522 * @Author 钱豹
... ... @@ -485,7 +652,11 @@ public class XlyErpService {
485 652 .build();
486 653 UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent);
487 654 // 初始化AiService 以防止热加载太慢 找不到相应的方法
488   - aiAgent.chat(userId, "initAiService");
  655 + try{
  656 + aiAgent.chat(userId, "initAiService");
  657 + }catch (Exception e){
  658 + e.printStackTrace();
  659 + }
489 660 log.info("用户{}Agent构建完成,已选场景:{},场景ID{}", userId, session.isSceneSelected() ? session.getCurrentScene().getSSceneName() : "未选(全场景匹配)", dynamicToolProvider.sSceneIdMap.get(userId));
490 661 }
491 662 return aiAgent;
... ...
src/main/java/com/xly/thread/AiUserAgentQuestionThread.java
1 1 package com.xly.thread;
2 2  
3 3  
  4 +import cn.hutool.core.lang.generator.UUIDGenerator;
4 5 import cn.hutool.core.util.ObjectUtil;
5 6 import cn.hutool.core.util.StrUtil;
6   -import com.xly.config.OperableChatMemoryProvider;
7 7 import com.xly.config.SpringContextHolder;
8 8 import com.xly.entity.UserSceneSession;
  9 +import com.xly.milvus.service.AiGlobalAgentQuestionSqlEmitterService;
9 10 import com.xly.service.DynamicExeDbService;
  11 +import com.xly.service.RedisService;
  12 +import com.xly.util.MD5Util;
  13 +import com.xly.util.SqlValidateUtil;
10 14 import dev.langchain4j.data.message.ChatMessage;
11 15 import dev.langchain4j.data.message.ChatMessageType;
12   -import jnr.ffi.annotations.In;
13   -
14   -import java.util.Arrays;
15 16 import java.util.HashMap;
16 17 import java.util.List;
17 18 import java.util.Map;
... ... @@ -36,17 +37,30 @@ public class AiUserAgentQuestionThread implements Runnable {
36 37 String sSceneId = session.getCurrentScene().getSId();
37 38 String sMethodId = session.getCurrentTool().getSId();
38 39 DynamicExeDbService dynamicExeDbService = SpringContextHolder.getBean(DynamicExeDbService.class);
  40 + RedisService redisService = SpringContextHolder.getBean(RedisService.class);
  41 + AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService = SpringContextHolder.getBean(AiGlobalAgentQuestionSqlEmitterService.class);
39 42 String sQuestionGroupNo = session.getSUserQuestionList().get(0);
40 43 Integer bRedis = (session.getSUserQuestionList().size()==1)?1:0;
41 44 Map<String, Object> data = getMap(sSceneId, sMethodId,bRedis,sQuestionGroupNo);
42   - data.put("sQuestion",getsQuestion(session.getSUserQuestionList()));
  45 + data.put("sQuestion",SqlValidateUtil.getsQuestion(session.getSUserQuestionList()));
  46 + data.put("sId",new UUIDGenerator().next());
  47 + //插入Redis缓存
  48 + if(bRedis==1 && ObjectUtil.isNotEmpty(sSqlContent)){
  49 + String sReidKey = SqlValidateUtil.getsKey( sSceneId, sMethodId, sQuestionGroupNo);
  50 + redisService.set(sReidKey,sSqlContent);
  51 + }
  52 + String sKey = sSceneId+"_"+sMethodId +"_"+sQuestion;
  53 +// SqlValidateUtil.getsKey( sSceneId, sMethodId, SqlValidateUtil.getsQuestion(session.getSUserQuestionList()));
  54 + //存入向量库
  55 + aiGlobalAgentQuestionSqlEmitterService.addAiGlobalAgentQuestionSqlEmitter(sKey,data,sQuestion,sSqlContent,"ai_global_agent_question_sql");
  56 + //调用数据库插入数据库
43 57 Map<String, Object> searMap = dynamicExeDbService.getDoProMap(sProName, data);
44 58 dynamicExeDbService.getCallPro(searMap, sProName);
45 59 }
46 60  
47   - private String getsQuestion(List<String> sUserQuestionList){
48   - return String.join(",", sUserQuestionList);
49   - }
  61 +
  62 +
  63 +
50 64  
51 65 //获取组ID
52 66 private String getQuestionGroupNo(){
... ...
src/main/java/com/xly/token/RedisTokenManager.java
1   -//package com.xly.token;
  1 +package com.xly.token;//package com.xly.token;
2 2 //
3 3 //import cn.hutool.core.util.ObjectUtil;
4 4 //import cn.hutool.core.util.StrUtil;
... ...
src/main/java/com/xly/tts/bean/TTSResponseDTO.java
... ... @@ -6,6 +6,7 @@ import lombok.AllArgsConstructor;
6 6 import lombok.Builder;
7 7 import lombok.Data;
8 8 import lombok.NoArgsConstructor;
  9 +import reactor.core.publisher.Flux;
9 10  
10 11 import java.io.Serializable;
11 12  
... ... @@ -70,6 +71,8 @@ public class TTSResponseDTO implements Serializable {
70 71  
71 72 private String sReturnType = ReturnTypeCode.MAKEDOWN.getCode();
72 73  
  74 + private Boolean ErpComplete;
  75 +
73 76 /**
74 77 * 创建失败响应
75 78 */
... ...
src/main/java/com/xly/tts/service/PythonTtsProxyService.java
... ... @@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
4 4 import cn.hutool.core.util.StrUtil;
5 5 import com.xly.constant.BusinessCode;
6 6 import com.xly.constant.ReturnTypeCode;
  7 +import com.xly.entity.AiResponseAccumulator;
7 8 import com.xly.entity.AiResponseDTO;
8 9 import com.xly.service.UserSceneSessionService;
9 10 import com.xly.service.XlyErpService;
... ... @@ -16,15 +17,15 @@ import org.springframework.core.io.InputStreamResource;
16 17 import org.springframework.http.*;
17 18 import org.springframework.stereotype.Service;
18 19 import org.springframework.web.client.RestTemplate;
  20 +import reactor.core.publisher.Flux;
19 21  
20 22 import javax.annotation.PostConstruct;
21 23 import java.io.*;
  24 +import java.time.Duration;
22 25 import java.util.*;
23 26 import java.util.concurrent.CompletableFuture;
24 27 import java.util.concurrent.ExecutorService;
25 28 import java.util.concurrent.Executors;
26   -import java.net.URL;
27   -import java.net.HttpURLConnection;
28 29 import java.io.InputStream;
29 30  
30 31 @Slf4j
... ... @@ -81,6 +82,111 @@ public class PythonTtsProxyService {
81 82 return synthesizeStreamAi(request, voiceText);
82 83 }
83 84  
  85 + /**
  86 + * 流式ERP + 流式TTS合成
  87 + * 先流式输出ERP文本,完成后自动开始TTS合成
  88 + * 使用现有TTSResponseDTO字段
  89 + */
  90 + public Flux<TTSResponseDTO> synthesizeStreamAiStream(TTSRequestDTO request) {
  91 + String userInput = request.getText();
  92 + String sUserId = request.getUserid();
  93 + String sUserName = request.getUsername();
  94 + String sBrandsId = request.getBrandsid();
  95 + String sSubsidiaryId = request.getSubsidiaryid();
  96 + String sUserType = request.getUsertype();
  97 + String authorization = request.getAuthorization();
  98 +
  99 + String requestId = UUID.randomUUID().toString();
  100 + log.info("开始流式处理: requestId={}, userId={}", requestId, sUserId);
  101 +
  102 + // 创建累积器(用于累积完整的AiResponseDTO)
  103 + AiResponseAccumulator accumulator = new AiResponseAccumulator(requestId);
  104 +
  105 + // 1. 处理ERP流,将AiResponseDTO转换为TTSResponseDTO
  106 + Flux<TTSResponseDTO> erpStream = xlyErpService.erpUserInputStream(
  107 + userInput, sUserId, sUserName, sBrandsId,
  108 + sSubsidiaryId, sUserType, authorization
  109 + )
  110 + .doOnNext(aiResponse -> {
  111 + // 设置请求ID
  112 + aiResponse.setRequestId(requestId);
  113 + // 后台累积完整文本(为后续TTS做准备)
  114 + accumulator.accumulate(aiResponse);
  115 + log.debug("收到ERP片段: requestId={}, chunk={}/{}",
  116 + requestId,
  117 + aiResponse.getChunkIndex(),
  118 + aiResponse.getTotalChunks());
  119 + })
  120 + .map(aiResponse -> {
  121 + // 将AiResponseDTO转换为TTSResponseDTO
  122 + // 使用processedText字段传递AI文本片段
  123 + // 使用systemText字段传递系统文本片段
  124 + return TTSResponseDTO.builder()
  125 + .code(200)
  126 + .message("ERP_CHUNK") // message字段标记为ERP文本块
  127 + .requestId(requestId)
  128 + .processedText(aiResponse.getTextFragment()) // 用processedText传递AI文本片段
  129 + .systemText(aiResponse.getSystemTextFragment()) // 用systemText传递系统文本片段
  130 + .sSceneName(aiResponse.getSSceneName())
  131 + .sMethodName(aiResponse.getSMethodName())
  132 + .sReturnType(aiResponse.getSReturnType())
  133 + .timestamp(System.currentTimeMillis())
  134 + .build();
  135 + });
  136 +
  137 + // 2. ERP完成后,发送完成标记,然后开始TTS合成
  138 + return erpStream
  139 + .concatWith(Flux.defer(() -> {
  140 + // 获取完整的累积结果
  141 + AiResponseDTO completeResponse = accumulator.getCompleteResponse();
  142 +
  143 + // 验证ERP结果
  144 + if (StrUtil.isBlank(completeResponse.getAiText())) {
  145 + log.warn("ERP返回空文本: requestId={}", requestId);
  146 + return Flux.error(new RuntimeException("ERP返回空文本"));
  147 + }
  148 +
  149 + log.info("ERP流式处理完成,开始TTS合成: requestId={}, aiText长度={}",
  150 + requestId, completeResponse.getAiText().length());
  151 +
  152 + // 3. 发送ERP完成消息(使用完整文本)
  153 + TTSResponseDTO erpCompleteDto = TTSResponseDTO.builder()
  154 + .code(200)
  155 + .message("ERP_COMPLETE") // message标记完成
  156 + .requestId(requestId)
  157 + .processedText(completeResponse.getAiText()) // 完整AI文本
  158 + .systemText(completeResponse.getSystemText()) // 完整系统文本
  159 + .sSceneName(completeResponse.getSSceneName())
  160 + .sMethodName(completeResponse.getSMethodName())
  161 + .sReturnType(completeResponse.getSReturnType())
  162 + .timestamp(System.currentTimeMillis())
  163 + .build();
  164 +
  165 + // 4. 调用TTS合成(返回TTSResponseDTO流)
  166 + Flux<TTSResponseDTO> ttsStream = synthesizeStreamAiNew(request, completeResponse)
  167 + .doOnNext(ttsResponse -> {
  168 + ttsResponse.setRequestId(requestId);
  169 + ttsResponse.setMessage("TTS_SEGMENT"); // message标记为TTS音频段
  170 + });
  171 +
  172 + // 先发送ERP完成消息,再发送TTS流
  173 + return Flux.concat(Flux.just(erpCompleteDto), ttsStream);
  174 + }))
  175 + // 超时控制
  176 + .timeout(Duration.ofSeconds(120))
  177 + // 错误处理
  178 + .onErrorResume(e -> {
  179 + log.error("流式处理失败: requestId={}, error={}", requestId, e.getMessage());
  180 + return Flux.just(TTSResponseDTO.error(requestId, 500, e.getMessage()));
  181 + })
  182 + // 日志记录
  183 + .doOnComplete(() -> log.info("流式处理完成: requestId={}", requestId))
  184 + .doOnCancel(() -> log.warn("流式处理取消: requestId={}", requestId));
  185 + }
  186 +
  187 +
  188 +
  189 +
84 190 public ResponseEntity<TTSResponseDTO> cleanMemory(TTSRequestDTO request) {
85 191 String sUserId = request.getUserid();
86 192 String sUserName = request.getUsername();
... ... @@ -123,7 +229,7 @@ public class PythonTtsProxyService {
123 229 }
124 230  
125 231 /**
126   - * 【保持原有返回类型】不动!内部流式请求Python
  232 + * 内部流式请求Python
127 233 */
128 234 public ResponseEntity<TTSResponseDTO> synthesizeStreamAi(TTSRequestDTO request, AiResponseDTO aiResponseDTO) {
129 235 String aiText = aiResponseDTO.getAiText();
... ... @@ -132,12 +238,10 @@ public class PythonTtsProxyService {
132 238 systemText = StrUtil.EMPTY;
133 239 }
134 240 String voiceTextNew = AdvancedSymbolRemover.removePunctuationHtml(aiText);
135   -
136 241 // ============================
137 242 // 【绝对唯一】不会重复、不会覆盖
138 243 // ============================
139 244 String cacheKey = request.getUserid() + "_" + System.nanoTime();
140   -
141 245 TTSResponseDTO dto = TTSResponseDTO.builder()
142 246 .code(200)
143 247 .message("success")
... ... @@ -159,7 +263,6 @@ public class PythonTtsProxyService {
159 263 if (!voiceless || ObjectUtil.isEmpty(voiceTextNew)) {
160 264 return ResponseEntity.ok(dto);
161 265 }
162   -
163 266 // 平均分割文字
164 267 List<String> textParts = splitTextSmart(voiceTextNew, 30);
165 268 dto.setAudioSize(textParts.size());
... ... @@ -199,14 +302,91 @@ public class PythonTtsProxyService {
199 302 }
200 303 }
201 304 }, executorService);
202   -
203 305 return ResponseEntity.ok(dto);
204 306 }
205 307  
  308 +
  309 + /**
  310 + * 内部流式请求Python
  311 + */
  312 + public Flux<TTSResponseDTO> synthesizeStreamAiNew(TTSRequestDTO request, AiResponseDTO aiResponseDTO) {
  313 + String aiText = aiResponseDTO.getAiText();
  314 + String systemText = aiResponseDTO.getSystemText();
  315 + if (ObjectUtil.isEmpty(systemText)) {
  316 + systemText = StrUtil.EMPTY;
  317 + }
  318 + String voiceTextNew = AdvancedSymbolRemover.removePunctuationHtml(aiText);
  319 + // ============================
  320 + // 【绝对唯一】不会重复、不会覆盖
  321 + // ============================
  322 + String cacheKey = request.getUserid() + "_" + System.nanoTime();
  323 + TTSResponseDTO dto = TTSResponseDTO.builder()
  324 + .code(200)
  325 + .message("success")
  326 + .cacheKey(cacheKey) // 前端靠这个取自己的分段
  327 + .originalText(request.getText())
  328 + .processedText(aiText)
  329 + .audioText(voiceTextNew)
  330 + .systemText(systemText)
  331 + .voice(request.getVoice())
  332 + .sSceneName(aiResponseDTO.getSSceneName())
  333 + .sMethodName(aiResponseDTO.getSMethodName())
  334 + .sReturnType(aiResponseDTO.getSReturnType())
  335 + .sCommonts(BusinessCode.COMMONTS.getMessage())
  336 + .timestamp(System.currentTimeMillis())
  337 + .textLength((aiText + systemText).length())
  338 + .build();
  339 +
  340 + boolean voiceless = Boolean.TRUE.equals(request.getVoiceless());
  341 + if (!voiceless || ObjectUtil.isEmpty(voiceTextNew)) {
  342 + return Flux.just(dto);
  343 + }
  344 + // 平均分割文字
  345 + List<String> textParts = splitTextSmart(voiceTextNew, 30);
  346 + dto.setAudioSize(textParts.size());
  347 + // 异步分段合成
  348 + CompletableFuture.runAsync(() -> {
  349 + for (int i = 0; i < textParts.size(); i++) {
  350 + String part = textParts.get(i);
  351 + if (ObjectUtil.isEmpty(part)) continue;
  352 +
  353 + try {
  354 + Map<String, Object> params = new HashMap<>();
  355 + params.put("text", part);
  356 + params.put("voice", request.getVoice());
  357 + params.put("rate", request.getRate() != null ? request.getRate() : "+10%");
  358 + params.put("volume", request.getVolume() != null ? request.getVolume() : "+0%");
  359 +
  360 + HttpHeaders headers = new HttpHeaders();
  361 + headers.setContentType(MediaType.APPLICATION_JSON);
  362 + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM));
  363 + HttpEntity<Map<String, Object>> entity = new HttpEntity<>(params, headers);
  364 +
  365 + ResponseEntity<byte[]> response = restTemplate.exchange(
  366 + pythonServiceUrl + "/stream-synthesize",
  367 + HttpMethod.POST, entity, byte[].class
  368 + );
  369 +
  370 + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
  371 + String base64 = Base64.getEncoder().encodeToString(response.getBody());
  372 +
  373 + // ============================
  374 + // 【关键】带序号存储!前端靠序号知道顺序!
  375 + // ============================
  376 + LocalAudioCache.addPiece(cacheKey, i, part, base64);
  377 + }
  378 + } catch (Exception e) {
  379 + log.warn("分段合成失败: {}", e.getMessage());
  380 + }
  381 + }
  382 + }, executorService);
  383 + return Flux.just(dto);
  384 + }
  385 +
  386 + // ==============================================
  387 + // 智能分段:优先按 。!?; , 空格 断开
  388 + // 不会把一句话生硬切断,更自然
206 389 // ==============================================
207   -// 智能分段:优先按 。!?; , 空格 断开
208   -// 不会把一句话生硬切断,更自然
209   -// ==============================================
210 390 private List<String> splitTextSmart(String text, int maxLength) {
211 391 List<String> parts = new ArrayList<>();
212 392 if (text == null || text.isEmpty()) return parts;
... ...
src/main/java/com/xly/util/MD5Util.java 0 → 100644
  1 +package com.xly.util;
  2 +
  3 +import java.security.MessageDigest;
  4 +import java.security.NoSuchAlgorithmException;
  5 +
  6 +/**
  7 + * MD5加密工具类
  8 + */
  9 +public class MD5Util {
  10 +
  11 + /**
  12 + * 将字符串进行MD5加密
  13 + * @param input 输入字符串
  14 + * @return MD5加密后的32位小写字符串
  15 + */
  16 + public static String encrypt(String input) {
  17 + if (input == null || input.isEmpty()) {
  18 + return null;
  19 + }
  20 +
  21 + try {
  22 + MessageDigest md = MessageDigest.getInstance("MD5");
  23 + byte[] messageDigest = md.digest(input.getBytes());
  24 +
  25 + // 将字节数组转换为十六进制字符串
  26 + StringBuilder hexString = new StringBuilder();
  27 + for (byte b : messageDigest) {
  28 + String hex = Integer.toHexString(0xff & b);
  29 + if (hex.length() == 1) {
  30 + hexString.append('0');
  31 + }
  32 + hexString.append(hex);
  33 + }
  34 + return hexString.toString();
  35 +
  36 + } catch (NoSuchAlgorithmException e) {
  37 + throw new RuntimeException("MD5加密失败", e);
  38 + }
  39 + }
  40 +
  41 + /**
  42 + * 将字符串进行MD5加密(大写)
  43 + * @param input 输入字符串
  44 + * @return MD5加密后的32位大写字符串
  45 + */
  46 + public static String encryptToUpperCase(String input) {
  47 + String result = encrypt(input);
  48 + return result != null ? result.toUpperCase() : null;
  49 + }
  50 +
  51 + /**
  52 + * 验证字符串与MD5值是否匹配
  53 + * @param input 输入字符串
  54 + * @param md5 MD5值
  55 + * @return 是否匹配
  56 + */
  57 + public static boolean verify(String input, String md5) {
  58 + String encrypted = encrypt(input);
  59 + return encrypted != null && encrypted.equalsIgnoreCase(md5);
  60 + }
  61 +
  62 + /**
  63 + * 获取文件的MD5值
  64 + * @param bytes 文件字节数组
  65 + * @return 文件MD5值
  66 + */
  67 + public static String getFileMD5(byte[] bytes) {
  68 + try {
  69 + MessageDigest md = MessageDigest.getInstance("MD5");
  70 + byte[] messageDigest = md.digest(bytes);
  71 +
  72 + StringBuilder hexString = new StringBuilder();
  73 + for (byte b : messageDigest) {
  74 + String hex = Integer.toHexString(0xff & b);
  75 + if (hex.length() == 1) {
  76 + hexString.append('0');
  77 + }
  78 + hexString.append(hex);
  79 + }
  80 + return hexString.toString();
  81 +
  82 + } catch (NoSuchAlgorithmException e) {
  83 + throw new RuntimeException("文件MD5计算失败", e);
  84 + }
  85 + }
  86 +
  87 + /**
  88 + * MD5加盐加密
  89 + * @param input 输入字符串
  90 + * @param salt 盐值
  91 + * @return 加盐后的MD5值
  92 + */
  93 + public static String encryptWithSalt(String input, String salt) {
  94 + return encrypt(input + salt);
  95 + }
  96 +
  97 + /**
  98 + * 双重MD5加密
  99 + * @param input 输入字符串
  100 + * @return 双重MD5加密结果
  101 + */
  102 + public static String doubleEncrypt(String input) {
  103 + String first = encrypt(input);
  104 + return encrypt(first);
  105 + }
  106 +}
0 107 \ No newline at end of file
... ...
src/main/java/com/xly/util/SqlValidateUtil.java
... ... @@ -103,4 +103,13 @@ public class SqlValidateUtil {
103 103 .replaceAll("\\n|\\r", " ")
104 104 .trim();
105 105 }
  106 +
  107 + public static String getsKey(String sSceneId,String sMethodId,String sQuestion){
  108 +// sSceneId+sMethodId+sQuestion
  109 + return sSceneId+"_"+sMethodId+"_"+ MD5Util.encrypt(sQuestion);
  110 + }
  111 +
  112 + public static String getsQuestion(List<String> sUserQuestionList){
  113 + return String.join(",", sUserQuestionList);
  114 + }
106 115 }
... ...
src/main/java/com/xly/web/TTSStreamController.java
... ... @@ -7,6 +7,7 @@ import com.xly.tool.DynamicToolProvider;
7 7 import com.xly.tts.bean.*;
8 8 import com.xly.tts.service.LocalAudioCache;
9 9 import com.xly.tts.service.PythonTtsProxyService;
  10 +import jakarta.validation.Valid;
10 11 import lombok.RequiredArgsConstructor;
11 12 import lombok.extern.slf4j.Slf4j;
12 13 import org.springframework.core.io.InputStreamResource;
... ... @@ -14,6 +15,8 @@ import org.springframework.http.MediaType;
14 15 import org.springframework.http.ResponseEntity;
15 16 import org.springframework.web.bind.annotation.*;
16 17 import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
  18 +import reactor.core.publisher.Flux;
  19 +import reactor.core.publisher.Mono;
17 20  
18 21 import javax.annotation.PostConstruct;
19 22 import javax.annotation.PreDestroy;
... ... @@ -82,6 +85,24 @@ public class TTSStreamController {
82 85 return pythonTtsProxyService.synthesizeStreamAi(request);
83 86 }
84 87  
  88 + /**
  89 + * 流式合成语音(代理到Python服务)
  90 + */
  91 + @PostMapping(value = "/stream/queryFlux",
  92 + consumes = {MediaType.APPLICATION_JSON_VALUE},
  93 + produces = {MediaType.APPLICATION_OCTET_STREAM_VALUE,
  94 + MediaType.APPLICATION_JSON_VALUE})
  95 + public Flux<TTSResponseDTO> streamFlux(@Valid @RequestBody Mono<TTSRequestDTO> requestMono) {
  96 + return requestMono.flatMapMany(request -> {
  97 + log.info("处理请求: requestId={}, text长度={}", request.getUserid(), request.getText().length());
  98 + return pythonTtsProxyService.synthesizeStreamAiStream(request);
  99 + })
  100 + .doOnSubscribe(sub -> log.debug("流式订阅开始"))
  101 + .doOnCancel(() -> log.debug("流式请求被取消"))
  102 + .doOnComplete(() -> log.debug("流式响应完成"))
  103 + .doOnError(e -> log.error("流式处理错误", e));
  104 + }
  105 +
85 106 @GetMapping("/audio/piece")
86 107 public ResponseEntity<Map<String, String>> getPiece(
87 108 @RequestParam String cacheKey,
... ... @@ -210,81 +231,6 @@ public class TTSStreamController {
210 231 }
211 232  
212 233 /**
213   - * SSE流式输出(Server-Sent Events)
214   - */
215   - @GetMapping(value = "/sse-stream", produces = "text/event-stream")
216   - public ResponseEntity<StreamingResponseBody> sseStream(
217   - @RequestParam String text,
218   - @RequestParam(defaultValue = "zh-CN-XiaoxiaoNeural") String voice) {
219   -
220   - log.info("收到SSE流式请求: voice={}", voice);
221   -
222   - TTSRequestDTO request = new TTSRequestDTO();
223   - request.setText(text);
224   - request.setVoice(voice);
225   -
226   - StreamingResponseBody responseBody = outputStream -> {
227   - try {
228   - outputStream.write(("event: audio-start\ndata: \n\n").getBytes());
229   - outputStream.flush();
230   -
231   - // 调用Python服务获取音频
232   - ResponseEntity<InputStreamResource> response = pythonTtsProxyService.synthesizeStream(request);
233   -
234   - if (response.getBody() != null) {
235   - InputStream inputStream = response.getBody().getInputStream();
236   - byte[] buffer = new byte[1024];
237   - int bytesRead;
238   -
239   - int totalBytes = 0;
240   - while ((bytesRead = inputStream.read(buffer)) != -1) {
241   - totalBytes += bytesRead;
242   -
243   - // 发送进度事件
244   - String progressEvent = String.format(
245   - "event: progress\ndata: {\"bytes\":%d}\n\n", totalBytes);
246   - outputStream.write(progressEvent.getBytes());
247   - outputStream.flush();
248   -
249   - // 发送音频数据(base64编码)
250   - String base64Data = java.util.Base64.getEncoder().encodeToString(
251   - java.util.Arrays.copyOfRange(buffer, 0, bytesRead));
252   - String audioEvent = String.format(
253   - "event: audio-data\ndata: {\"chunk\":\"%s\"}\n\n", base64Data);
254   - outputStream.write(audioEvent.getBytes());
255   - outputStream.flush();
256   - }
257   -
258   - // 发送完成事件
259   - String completeEvent = String.format(
260   - "event: audio-complete\ndata: {\"total_bytes\":%d}\n\n", totalBytes);
261   - outputStream.write(completeEvent.getBytes());
262   - outputStream.flush();
263   - } else {
264   - outputStream.write(("event: error\ndata: {\"message\":\"合成失败\"}\n\n").getBytes());
265   - outputStream.flush();
266   - }
267   -
268   - } catch (Exception e) {
269   -// log.error("SSE流式输出异常: {}", e.getMessage(), e);
270   - try {
271   - outputStream.write(("event: error\ndata: {\"message\":\"" +
272   - e.getMessage().replace("\"", "\\\"") + "\"}\n\n").getBytes());
273   - outputStream.flush();
274   - } catch (Exception ex) {
275   - // 忽略关闭错误
276   - }
277   - }
278   - };
279   -
280   - return ResponseEntity.ok()
281   - .header("Content-Type", "text/event-stream")
282   - .header("Cache-Control", "no-cache")
283   - .header("X-Accel-Buffering", "no") // 禁用Nginx缓冲
284   - .body(responseBody);
285   - }
286   -
287   - /**
288 234 * 测试接口
289 235 */
290 236 @GetMapping("/test")
... ...