From fb5dcaeb2ab91d0f9ffea26fd15ddcbbe5d36bb9 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 31 七月 2026 17:57:27 +0800
Subject: [PATCH] feat(aftersales): 售后工单新增问题类型和严重程度字段

---
 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java |  487 ++++++++++++++++++++++++++++++++++-------------------
 1 files changed, 312 insertions(+), 175 deletions(-)

diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java
index 69cd74e..c11c264 100644
--- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java
+++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java
@@ -1,226 +1,363 @@
 package cn.iocoder.yudao.module.ai.service.knowledge;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.StrUtil;
-import cn.hutool.http.HttpUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.*;
+import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
 import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper;
+import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
+import cn.iocoder.yudao.module.ai.service.model.AiModelService;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.ai.document.Document;
-import org.springframework.ai.reader.tika.TikaDocumentReader;
-import org.springframework.ai.tokenizer.TokenCountEstimator;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.core.io.ByteArrayResource;
+import org.springframework.ai.transformer.splitter.TokenTextSplitter;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import org.apache.tika.parser.AutoDetectParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.sax.BodyContentHandler;
+import org.xml.sax.ContentHandler;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
 
-/**
- * AI 鐭ヨ瘑搴撴枃妗� Service 瀹炵幇绫�
- *
- * @author xiaoxin
- */
-@Service
 @Slf4j
+@Service
 public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentService {
 
     @Resource
-    private AiKnowledgeDocumentMapper knowledgeDocumentMapper;
-
+    private AiKnowledgeDocumentMapper documentMapper;
     @Resource
-    private TokenCountEstimator tokenCountEstimator;
-
+    private AiKnowledgeSegmentMapper segmentMapper;
     @Resource
-    private AiKnowledgeSegmentService knowledgeSegmentService;
-    @Resource
-    @Lazy // 寤惰繜鍔犺浇锛岄伩鍏嶅惊鐜緷璧�
     private AiKnowledgeService knowledgeService;
-
-    @Override
-    public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) {
-        // 1. 鏍¢獙鍙傛暟
-        knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId());
-
-        // 2. 涓嬭浇鏂囨。
-        String content = readUrl(createReqVO.getUrl());
-
-        // 3. 鏂囨。璁板綍鍏ュ簱
-        AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class)
-                .setContent(content).setContentLength(content.length()).setTokens(tokenCountEstimator.estimate(content))
-                .setStatus(CommonStatusEnum.ENABLE.getStatus());
-        knowledgeDocumentMapper.insert(documentDO);
-
-        // 4. 鏂囨。鍒囩墖鍏ュ簱锛堝紓姝ワ級
-        knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(documentDO.getId(), content);
-        return documentDO.getId();
-    }
-
-    @Override
-    public List<Long> createKnowledgeDocumentList(AiKnowledgeDocumentCreateListReqVO createListReqVO) {
-        // 1. 鏍¢獙鍙傛暟
-        knowledgeService.validateKnowledgeExists(createListReqVO.getKnowledgeId());
-
-        // 2. 涓嬭浇鏂囨。
-        List<String> contents = convertList(createListReqVO.getList(), document -> readUrl(document.getUrl()));
-
-        // 3. 鏂囨。璁板綍鍏ュ簱
-        List<AiKnowledgeDocumentDO> documentDOs = new ArrayList<>(createListReqVO.getList().size());
-        for (int i = 0; i < createListReqVO.getList().size(); i++) {
-            AiKnowledgeDocumentCreateListReqVO.Document documentVO = createListReqVO.getList().get(i);
-            String content = contents.get(i);
-            documentDOs.add(BeanUtils.toBean(documentVO, AiKnowledgeDocumentDO.class)
-                    .setKnowledgeId(createListReqVO.getKnowledgeId())
-                    .setContent(content).setContentLength(content.length())
-                    .setTokens(tokenCountEstimator.estimate(content))
-                    .setSegmentMaxTokens(createListReqVO.getSegmentMaxTokens())
-                    .setStatus(CommonStatusEnum.ENABLE.getStatus()));
-        }
-        knowledgeDocumentMapper.insertBatch(documentDOs);
-
-        // 4. 鎵归噺鍒涘缓鏂囨。鍒囩墖锛堝紓姝ワ級
-        documentDOs.forEach(documentDO -> knowledgeSegmentService
-                .createKnowledgeSegmentBySplitContentAsync(documentDO.getId(), documentDO.getContent()));
-        return convertList(documentDOs, AiKnowledgeDocumentDO::getId);
-    }
-
-    @Override
-    public PageResult<AiKnowledgeDocumentDO> getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO) {
-        return knowledgeDocumentMapper.selectPage(pageReqVO);
-    }
-
-    @Override
-    public AiKnowledgeDocumentDO getKnowledgeDocument(Long id) {
-        return knowledgeDocumentMapper.selectById(id);
-    }
-
-    @Override
-    public void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO) {
-        // 1. 鏍¢獙鏂囨。鏄惁瀛樺湪
-        AiKnowledgeDocumentDO oldDocument = validateKnowledgeDocumentExists(reqVO.getId());
-
-        // 2. 鏇存柊鏂囨。
-        AiKnowledgeDocumentDO document = BeanUtils.toBean(reqVO, AiKnowledgeDocumentDO.class);
-        knowledgeDocumentMapper.updateById(document);
-
-        // 3. 濡傛灉澶勪簬寮�鍚姸鎬侊紝骞朵笖鏈�澶� tokens 鍙戠敓鍙樺寲锛屽垯 segment 闇�瑕侀噸鏂扮储寮�
-        if (CommonStatusEnum.isEnable(oldDocument.getStatus())
-                && reqVO.getSegmentMaxTokens() != null
-                && ObjUtil.notEqual(reqVO.getSegmentMaxTokens(), oldDocument.getSegmentMaxTokens())) {
-            // 鍒犻櫎鏃х殑鏂囨。鍒囩墖
-            knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(reqVO.getId());
-            // 閲嶆柊鍒涘缓鏂囨。鍒囩墖
-            knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(reqVO.getId(), oldDocument.getContent());
-        }
-    }
-
-    @Override
-    public void updateKnowledgeDocumentStatus(AiKnowledgeDocumentUpdateStatusReqVO reqVO) {
-        // 1. 鏍¢獙瀛樺湪
-        AiKnowledgeDocumentDO document = validateKnowledgeDocumentExists(reqVO.getId());
-
-        // 2. 鏇存柊鐘舵��
-        knowledgeDocumentMapper.updateById(new AiKnowledgeDocumentDO()
-                .setId(reqVO.getId()).setStatus(reqVO.getStatus()));
-
-        // 3. 澶勭悊鏂囨。鍒囩墖
-        if (CommonStatusEnum.isEnable(reqVO.getStatus())) {
-            knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(reqVO.getId(), document.getContent());
-        } else {
-            knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(reqVO.getId());
-        }
-    }
+    @Resource
+    private AiKnowledgeSegmentService segmentService;
+    @Resource
+    private AiModelService modelService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void deleteKnowledgeDocument(Long id) {
-        // 1. 鏍¢獙瀛樺湪
-        validateKnowledgeDocumentExists(id);
-
-        // 2. 鍒犻櫎
-        knowledgeDocumentMapper.deleteById(id);
-
-        // 3. 鍒犻櫎瀵瑰簲鐨勬钀�
-        knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(id);
-    }
-
-    @Override
-    public AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) {
-        AiKnowledgeDocumentDO knowledgeDocument = knowledgeDocumentMapper.selectById(id);
-        if (knowledgeDocument == null) {
-            throw exception(KNOWLEDGE_DOCUMENT_NOT_EXISTS);
+    public List<Long> createDocuments(AiKnowledgeDocumentCreateReqVO createReqVO) {
+        AiKnowledgeDO knowledge = knowledgeService.validateKnowledge(createReqVO.getKnowledgeId());
+        List<AiKnowledgeDocumentCreateReqVO.DocumentItem> list = createReqVO.getList();
+        if (CollUtil.isEmpty(list)) {
+            throw new IllegalArgumentException("鏂囨。鍒楄〃涓嶈兘涓虹┖");
         }
-        return knowledgeDocument;
-    }
+        int defaultSegmentMaxTokens = createReqVO.getSegmentMaxTokens() != null
+                ? createReqVO.getSegmentMaxTokens() : 800;
 
-    @Override
-    public String readUrl(String url) {
-        // 涓嬭浇鏂囦欢
-        ByteArrayResource resource;
-        try {
-            byte[] bytes = HttpUtil.downloadBytes(url);
-            if (bytes.length == 0) {
-                throw exception(KNOWLEDGE_DOCUMENT_FILE_EMPTY);
+        List<Long> ids = new ArrayList<>();
+        for (AiKnowledgeDocumentCreateReqVO.DocumentItem item : list) {
+            AiKnowledgeDocumentCreateReqVO.UrlInfo urlInfo = item.getUrl();
+            String fileUrl = urlInfo != null ? urlInfo.getUrl() : null;
+            AiKnowledgeDocumentDO document = new AiKnowledgeDocumentDO();
+            document.setKnowledgeId(knowledge.getId());
+            document.setName(item.getName());
+            document.setUrl(fileUrl);
+            document.setStatus(CommonStatusEnum.ENABLE.getStatus());
+            document.setSegmentMaxTokens(defaultSegmentMaxTokens);
+            documentMapper.insert(document);
+            ids.add(document.getId());
+
+            if (fileUrl != null) {
+                try {
+                    loadDocumentContent(document);
+                    documentMapper.updateById(document);
+                    // 鑷姩鎵ц鍒嗘鍜屽悜閲忓寲
+                    processDocumentSegmentsInternal(document, knowledge);
+                } catch (Exception e) {
+                    log.error("鏂囨。[{}]澶勭悊澶辫触: {}", document.getId(), e.getMessage());
+                    document.setStatus(CommonStatusEnum.DISABLE.getStatus());
+                    documentMapper.updateById(document);
+                }
             }
-            resource = new ByteArrayResource(bytes);
+        }
+        return ids;
+    }
+
+    private void loadDocumentContent(AiKnowledgeDocumentDO document) {
+        try {
+            byte[] fileBytes = downloadFile(document.getUrl());
+            String content = extractText(fileBytes, document.getName());
+            if (StrUtil.isEmpty(content)) {
+                content = new String(fileBytes, java.nio.charset.Charset.forName("UTF-8"));
+            }
+            document.setContent(content);
+            document.setContentLength(content.length());
+            document.setTokens(estimateTokens(content));
         } catch (Exception e) {
-            log.error("[readUrl][url({}) 璇诲彇澶辫触]", url, e);
-            throw exception(KNOWLEDGE_DOCUMENT_FILE_DOWNLOAD_FAIL);
+            throw new RuntimeException("鏂囨。鍐呭鍔犺浇澶辫触: " + e.getMessage(), e);
         }
-
-        // 璇诲彇鏂囦欢
-        TikaDocumentReader loader = new TikaDocumentReader(resource);
-        List<Document> documents = loader.get();
-        Document document = CollUtil.getFirst(documents);
-        if (document == null || StrUtil.isEmpty(document.getText())) {
-            throw exception(KNOWLEDGE_DOCUMENT_FILE_READ_FAIL);
-        }
-        return document.getText();
     }
 
-    @Override
-    public List<AiKnowledgeDocumentDO> getKnowledgeDocumentList(Collection<Long> ids) {
-        if (CollUtil.isEmpty(ids)) {
-            return Collections.emptyList();
+    private byte[] downloadFile(String fileUrl) {
+        try {
+            // 鎵嬪姩缂栫爜 URL 璺緞涓殑闈� ASCII 瀛楃锛岄伩鍏� URI 鏋勯�犲嚱鏁版姤閿�
+            String encodedUrl = encodeUrlPath(fileUrl);
+            URL url = URL.of(new URI(encodedUrl), null);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            conn.setConnectTimeout(30000);
+            conn.setReadTimeout(60000);
+            conn.setRequestMethod("GET");
+            try (InputStream is = conn.getInputStream();
+                 ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+                IoUtil.copy(is, bos);
+                return bos.toByteArray();
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("鏂囦欢涓嬭浇澶辫触: " + e.getMessage(), e);
         }
-        return knowledgeDocumentMapper.selectByIds(ids);
     }
 
-    @Override
-    public List<AiKnowledgeDocumentDO> getKnowledgeDocumentListByKnowledgeId(Long knowledgeId) {
-        return knowledgeDocumentMapper.selectListByKnowledgeId(knowledgeId);
+    private static String encodeUrlPath(String urlString) {
+        // 鍒嗙 scheme://authority 鍜� path?query#fragment
+        int schemeEnd = urlString.indexOf("://");
+        if (schemeEnd < 0) return urlString;
+        int pathStart = urlString.indexOf('/', schemeEnd + 3);
+        if (pathStart < 0) return urlString; // 鏃� path
+
+        String base = urlString.substring(0, pathStart);
+        String pathAndRest = urlString.substring(pathStart);
+
+        // 鍒嗙 path 鍜� query + fragment
+        int queryStart = pathAndRest.indexOf('?');
+        int fragStart = pathAndRest.indexOf('#');
+        String rawPath, query, fragment;
+        if (queryStart >= 0) {
+            rawPath = pathAndRest.substring(0, queryStart);
+            if (fragStart >= 0 && fragStart > queryStart) {
+                query = pathAndRest.substring(queryStart, fragStart);
+                fragment = pathAndRest.substring(fragStart);
+            } else {
+                query = pathAndRest.substring(queryStart);
+                fragment = "";
+            }
+        } else if (fragStart >= 0) {
+            rawPath = pathAndRest.substring(0, fragStart);
+            query = "";
+            fragment = pathAndRest.substring(fragStart);
+        } else {
+            rawPath = pathAndRest;
+            query = "";
+            fragment = "";
+        }
+
+        // 瀵硅矾寰勬瘡涓鍋� URL 缂栫爜
+        StringBuilder encodedPath = new StringBuilder();
+        for (String segment : rawPath.split("/")) {
+            if (!segment.isEmpty()) {
+                encodedPath.append("/").append(URLEncoder.encode(segment, StandardCharsets.UTF_8)
+                        .replace("+", "%20"));
+            }
+        }
+        if (rawPath.endsWith("/")) encodedPath.append("/");
+
+        return base + encodedPath + query + fragment;
+    }
+
+    private String extractText(byte[] fileBytes, String fileName) {
+        try {
+            String ext = FileUtil.extName(fileName).toLowerCase();
+            if (ArrayUtil.contains(new String[]{"txt", "md", "json", "xml", "csv", "yaml", "yml"}, ext)) {
+                return new String(fileBytes, StandardCharsets.UTF_8);
+            }
+            // 浣跨敤 AutoDetectParser + EmbeddedDocumentExtractor 璺宠繃宓屽叆鍥剧墖锛岄伩鍏嶆彁鍙栧埌浜岃繘鍒朵贡鐮�
+            Parser parser = new AutoDetectParser();
+            ParseContext context = new ParseContext();
+            context.set(EmbeddedDocumentExtractor.class, new EmbeddedDocumentExtractor() {
+                @Override
+                public boolean shouldParseEmbedded(org.apache.tika.metadata.Metadata metadata) {
+                    return false;
+                }
+                @Override
+                public void parseEmbedded(InputStream inputStream, ContentHandler contentHandler,
+                                          org.apache.tika.metadata.Metadata metadata, boolean outputHtml) {
+                }
+            });
+            BodyContentHandler handler = new BodyContentHandler(-1);
+            try (InputStream is = new java.io.ByteArrayInputStream(fileBytes)) {
+                parser.parse(is, handler, new org.apache.tika.metadata.Metadata(), context);
+            }
+            return handler.toString().trim();
+        } catch (Exception e) {
+            log.warn("Tika 瑙f瀽鏂囨。澶辫触锛屽皾璇� UTF-8 鏂囨湰璇诲彇: {}", e.getMessage());
+            return "";
+        }
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void deleteKnowledgeDocumentByKnowledgeId(Long knowledgeId) {
-        // 1. 鑾峰彇璇ョ煡璇嗗簱涓嬬殑鎵�鏈夋枃妗�
-        List<AiKnowledgeDocumentDO> documents = knowledgeDocumentMapper.selectListByKnowledgeId(knowledgeId);
-        if (CollUtil.isEmpty(documents)) {
-            return;
+    public void updateDocument(AiKnowledgeDocumentUpdateReqVO updateReqVO) {
+        AiKnowledgeDocumentDO document = validateDocumentExists(updateReqVO.getId());
+        boolean urlChanged = StrUtil.isNotEmpty(updateReqVO.getUrl())
+                && !updateReqVO.getUrl().equals(document.getUrl());
+        if (StrUtil.isNotEmpty(updateReqVO.getName())) document.setName(updateReqVO.getName());
+        if (urlChanged) document.setUrl(updateReqVO.getUrl());
+        documentMapper.updateById(document);
+        // URL 鍙樻洿鏃堕噸鏂版彁鍙栧唴瀹广�侀噸鏂板垎娈靛拰鍚戦噺鍖�
+        if (urlChanged) {
+            try {
+                loadDocumentContent(document);
+                documentMapper.updateById(document);
+                AiKnowledgeDO knowledge = knowledgeService.validateKnowledge(document.getKnowledgeId());
+                // 鍒犻櫎鏃у垎娈靛拰鍚戦噺
+                segmentService.deleteSegmentsByDocumentId(document.getId());
+                // 閲嶆柊鍒嗘 + 鍚戦噺鍖�
+                processDocumentSegmentsInternal(document, knowledge);
+            } catch (Exception e) {
+                log.error("鏂囨。[{}] URL 鏇存柊鍚庡鐞嗗け璐�: {}", document.getId(), e.getMessage());
+                document.setStatus(CommonStatusEnum.DISABLE.getStatus());
+                documentMapper.updateById(document);
+            }
         }
+    }
 
-        // 2. 閫愪釜鍒犻櫎鏂囨。鍙婂叾瀵瑰簲鐨勬钀�
-        for (AiKnowledgeDocumentDO document : documents) {
-            deleteKnowledgeDocument(document.getId());
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteDocument(Long id) {
+        AiKnowledgeDocumentDO document = validateDocumentExists(id);
+        segmentService.deleteSegmentsByDocumentId(id);
+        documentMapper.deleteById(id);
+    }
+
+    @Override
+    public AiKnowledgeDocumentDO getDocument(Long id) {
+        return documentMapper.selectById(id);
+    }
+
+    @Override
+    public AiKnowledgeDocumentDO validateDocumentExists(Long id) {
+        AiKnowledgeDocumentDO document = documentMapper.selectById(id);
+        if (document == null) throw exception(KNOWLEDGE_DOCUMENT_NOT_EXISTS);
+        return document;
+    }
+
+    @Override
+    public PageResult<AiKnowledgeDocumentDO> getDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO) {
+        return documentMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateDocumentStatus(AiKnowledgeDocumentUpdateStatusReqVO updateStatusReqVO) {
+        AiKnowledgeDocumentDO document = validateDocumentExists(updateStatusReqVO.getId());
+        document.setStatus(updateStatusReqVO.getStatus());
+        documentMapper.updateById(document);
+    }
+
+    @Override
+    public List<AiKnowledgeSegmentProcessRespVO> getDocumentProcessingProgress(List<Long> documentIds) {
+        if (CollUtil.isEmpty(documentIds)) return Collections.emptyList();
+        return segmentMapper.selectProcessList(documentIds);
+    }
+
+    @Override
+    public void processDocumentSegments(Long documentId) {
+        AiKnowledgeDocumentDO document = validateDocumentExists(documentId);
+        if (StrUtil.isEmpty(document.getContent())) {
+            throw exception(KNOWLEDGE_DOCUMENT_FILE_EMPTY);
         }
+        AiKnowledgeDO knowledge = knowledgeService.validateKnowledge(document.getKnowledgeId());
+        processDocumentSegmentsInternal(document, knowledge);
+    }
+
+    private void processDocumentSegmentsInternal(AiKnowledgeDocumentDO document, AiKnowledgeDO knowledge) {
+        if (StrUtil.isEmpty(document.getContent())) return;
+
+        AiModelDO embeddingModel = modelService.validateModel(knowledge.getEmbeddingModelId());
+
+        // 鍒犻櫎鏃у垎娈�
+        segmentService.deleteSegmentsByDocumentId(document.getId());
+
+        // 鏂囨湰鍒囩墖
+        int segmentMaxTokens = document.getSegmentMaxTokens() != null ? document.getSegmentMaxTokens() : 800;
+        List<String> segmentTexts = splitContent(document.getContent(), segmentMaxTokens);
+        if (CollUtil.isEmpty(segmentTexts)) return;
+
+        // 鍚戦噺鍖栧苟瀛樺偍
+        List<AiKnowledgeSegmentDO> segments = new ArrayList<>();
+        for (String content : segmentTexts) {
+            if (StrUtil.isEmpty(content.trim())) continue;
+            segments.add(buildSegmentDO(knowledge.getId(), document.getId(), content));
+        }
+        segmentService.saveSegments(segments, knowledge.getId(), embeddingModel.getId());
+    }
+
+    private List<String> splitContent(String content, int maxTokens) {
+        TokenTextSplitter splitter = TokenTextSplitter.builder()
+                .withChunkSize(maxTokens)
+                .withMinChunkSizeChars(50)
+                .withMinChunkLengthToEmbed(10)
+                .withMaxNumChunks(1000)
+                .withKeepSeparator(true)
+                .build();
+        List<Document> docs = splitter.apply(Collections.singletonList(new Document(content)));
+        List<String> result = new ArrayList<>();
+        for (Document doc : docs) {
+            if (StrUtil.isNotEmpty(doc.getText())) {
+                result.add(doc.getText().trim());
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public List<String> previewSplit(String url, String name, Integer segmentMaxTokens) {
+        byte[] fileBytes = downloadFile(url);
+        String text = extractText(fileBytes, name);
+        if (StrUtil.isEmpty(text)) {
+            text = new String(fileBytes, java.nio.charset.Charset.forName("UTF-8"));
+        }
+        int maxTokens = segmentMaxTokens != null && segmentMaxTokens > 0 ? segmentMaxTokens : 800;
+        return splitContent(text, maxTokens);
+    }
+
+    private AiKnowledgeSegmentDO buildSegmentDO(Long knowledgeId, Long documentId, String content) {
+        AiKnowledgeSegmentDO segment = new AiKnowledgeSegmentDO();
+        segment.setKnowledgeId(knowledgeId);
+        segment.setDocumentId(documentId);
+        segment.setContent(content);
+        segment.setContentLength(content.length());
+        segment.setTokens(estimateTokens(content));
+        segment.setStatus(CommonStatusEnum.ENABLE.getStatus());
+        segment.setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY);
+        segment.setRetrievalCount(0);
+        return segment;
+    }
+
+    private Integer estimateTokens(String text) {
+        if (StrUtil.isEmpty(text)) return 0;
+        int chineseChars = 0, englishWords = 0;
+        for (char c : text.toCharArray()) {
+            if (c >= 0x4E00 && c <= 0x9FA5) chineseChars++;
+        }
+        for (String word : text.split("\\s+")) {
+            if (word.matches(".*[a-zA-Z].*")) englishWords++;
+        }
+        return chineseChars + (int) (englishWords * 1.3);
     }
 
 }

--
Gitblit v1.9.3