Sfoglia il codice sorgente

Merge remote-tracking branch 'origin/master'

lusa 3 settimane fa
parent
commit
cfd9b3b5a4
43 ha cambiato i file con 1592 aggiunte e 374 eliminazioni
  1. 17 17
      Jenkinsfile
  2. 5 0
      java/storlead-api/src/main/resources/application-dev.yml
  3. 5 0
      java/storlead-api/src/main/resources/application-test.yml
  4. 4 0
      java/storlead-api/src/main/resources/application.yml
  5. 1 2
      java/storlead-dependencies/pom.xml
  6. 5 0
      java/storlead-framework/storlead-common/src/main/java/com/storlead/framework/common/constant/DSConstants.java
  7. 153 10
      java/storlead-knowledge/storlead-knowledge-api/src/main/java/com/storlead/knowledge/api/ChunkController.java
  8. 119 0
      java/storlead-knowledge/storlead-knowledge-api/src/main/java/com/storlead/knowledge/api/DocumentController.java
  9. 0 3
      java/storlead-knowledge/storlead-knowledge-biz/src/main/java/com/storlead/knowledge/service/impl/ChatServiceImpl.java
  10. 3 1
      java/storlead-knowledge/storlead-knowledge-core/src/main/java/com/storlead/knowledge/utils/HttpService.java
  11. 12 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/config/TradeSchedulingConfig.java
  12. 25 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/constant/CustomerAnalysisConstants.java
  13. 34 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/CustomerAiAnalysisController.java
  14. 175 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/MarketingCampaignSopDetailController.java
  15. 14 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/MarketingController.java
  16. 3 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketingCampaignDTO.java
  17. 51 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketingCampaignSopDetailDTO.java
  18. 5 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/SopDetailCreateDTO.java
  19. 5 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/SopDetailUpdateDTO.java
  20. 3 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/CustomerEntity.java
  21. 4 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/MarketingCampaignEntity.java
  22. 55 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/MarketingCampaignSopDetailEntity.java
  23. 4 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/SopDetailEntity.java
  24. 12 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/mapper/MarketingCampaignSopDetailMapper.java
  25. 14 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/CustomerAnalysisDispatchService.java
  26. 1 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/MarketingCampaignEntityService.java
  27. 10 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/MarketingCampaignSopDetailService.java
  28. 104 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/CustomerAnalysisDispatchServiceImpl.java
  29. 69 6
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignEntityServiceImpl.java
  30. 16 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignSopDetailServiceImpl.java
  31. 31 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/task/InvalidCustomerAnalysisDispatchTask.java
  32. 10 0
      java/storlead-sasa/storlead-trade/src/main/resources/sql/customer_add_next_analysis_date.sql
  33. 2 0
      ui/smarttrade-platform/.env.development
  34. 2 0
      ui/smarttrade-platform/.env.production
  35. 9 0
      ui/smarttrade-platform/package-lock.json
  36. 1 0
      ui/smarttrade-platform/package.json
  37. 31 1
      ui/smarttrade-platform/src/api/knowledge/index.js
  38. 6 1
      ui/smarttrade-platform/src/api/layout/index.js
  39. 2 0
      ui/smarttrade-platform/src/assets/styles/main.css
  40. 13 3
      ui/smarttrade-platform/src/components/layout/Sidebar.vue
  41. 162 245
      ui/smarttrade-platform/src/components/layout/TopBar.vue
  42. 20 4
      ui/smarttrade-platform/src/views/CustomersView.vue
  43. 375 81
      ui/smarttrade-platform/src/views/KnowledgeView.vue

+ 17 - 17
Jenkinsfile

@@ -4,7 +4,7 @@ properties([
                  choiceType: 'PT_SINGLE_SELECT',
                  description: '选择要部署 API or UI',
                  name: 'modulePrefix',
-                 randomName: 'choice-parameter-5631314439613978',
+                 randomName: 'choice-parameter-56313144329613978',
                  script: [
                          $class: 'GroovyScript',
                          fallbackScript: [
@@ -25,7 +25,7 @@ properties([
                  choiceType: 'PT_SINGLE_SELECT',
                  description: '选择要部署的项目',
                  name: 'module',
-                 randomName: 'choice-parameter-5631314456178619',
+                 randomName: 'choice-parameter-56313144562178619',
                  referencedParameters: 'modulePrefix',
                  script: [
                          $class: 'GroovyScript',
@@ -39,13 +39,13 @@ properties([
                                  classpath: [],
                                  sandbox: true,
                                  script:
-                                         ''' if (modulePrefix.equals("java")){
-                                return["storlead-centre-api"]
-                            }
-                            else if(modulePrefix.equals("ui")){
-                                return["sp-user-center"]
-                            }
-                        '''
+                                    ''' if (modulePrefix.equals("java")){
+                                            return["storlead-api"]
+                                        }
+                                        else if(modulePrefix.equals("ui")){
+                                            return["smarttrade-platform","su-user-center"]
+                                        }
+                                    '''
                          ]
                  ]
                 ]
@@ -133,9 +133,9 @@ pipeline {
 
                         echo "处理MODULE_NAMESPACE_RESTFUL"
                         //把 module 名 从 sp-xxx 变成 xxx 也就是 项目 的 namespace
-                        if (API_NAMESPACE_RESTFUL == "storlead-centre-api") {
-                            API_NAMESPACE_RESTFUL = "api"
-                        }
+//                         if (API_NAMESPACE_RESTFUL == "storlead-centre-api") {
+//                             API_NAMESPACE_RESTFUL = "api"
+//                         }
                         echo "处理镜像推送"
                     }
                 }
@@ -177,11 +177,11 @@ pipeline {
                             remote.user = usernameHW
                             remote.password = passwordHW
                         }
-                        if(params.module == 'sp-user-mobile-center') {
+//                         if (params.module == 'sp-user-mobile-center') {
                            sh """cd ${params.modulePrefix}/${params.module} && npm install  && npm run build:${params.profile}"""
-                        } else {
-                           sh """cd ${params.modulePrefix}/${params.module} && npm install  && npm run ${params.profile}"""
-                        }
+//                         } else {
+//                            sh """cd ${params.modulePrefix}/${params.module} && npm install  && npm run ${params.profile}"""
+//                         }
 
 //                          sh """cd ${params.modulePrefix}/${params.module}  && npm run ${params.profile}"""
 
@@ -190,7 +190,7 @@ pipeline {
                         sshCommand remote: remote, command: "rm -rf /app/sp/ui/app-centre/${params.profile}/${params.module}/dist/*"
 
 
-                        echo "/app/sp/ui/sales/${params.profile}/${params.module}/*"
+                        echo "/app/sp/ui/app-centre/${params.profile}/${params.module}/*"
 
                         sshPut remote: remote, from: "${params.modulePrefix}/${params.module}/dist/", into: "/app/sp/ui/app-centre/${params.profile}/${params.module}/"
 

+ 5 - 0
java/storlead-api/src/main/resources/application-dev.yml

@@ -94,6 +94,11 @@ spring:
           username: root
           password: rCgRgLjH99Xvg5BN
           driver-class-name: com.mysql.jdbc.Driver
+        trade:
+            url: jdbc:mysql://mysql.test.storlead.com:39091/sp_trade_platform_test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true
+            username: root
+            password: rCgRgLjH99Xvg5BN
+            driver-class-name: com.mysql.jdbc.Driver
         sales:
           driver-class-name: com.mysql.jdbc.Driver
           url: jdbc:mysql://mysql.test.storlead.com:39091/sp_sales_test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true

+ 5 - 0
java/storlead-api/src/main/resources/application-test.yml

@@ -93,6 +93,11 @@ spring:
           username: root
           password: rCgRgLjH99Xvg5BN
           driver-class-name: com.mysql.jdbc.Driver
+        trade:
+          url: jdbc:mysql://mysql.test.storlead.com:39091/sp_trade_platform_test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true
+          username: root
+          password: rCgRgLjH99Xvg5BN
+          driver-class-name: com.mysql.jdbc.Driver
         sales:
           driver-class-name: com.mysql.jdbc.Driver
           url: jdbc:mysql://mysql.test.storlead.com:39091/sp_sales_test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true

+ 4 - 0
java/storlead-api/src/main/resources/application.yml

@@ -88,6 +88,10 @@ storlead:
     # 子模块默认数据源(未标注 @DS 时生效;标注 @DS 则以注解为准)
     module-default-enabled: true
     module-defaults:
+      - packages:
+          - com.storlead.trade.service
+          - com.storlead.trade.mapper
+        datasource: trade
       - packages:
           - com.storlead.sales.service
           - com.storlead.sales.mapper

+ 1 - 2
java/storlead-dependencies/pom.xml

@@ -6,7 +6,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.storlead.boot</groupId>
     <artifactId>storlead-dependencies</artifactId>
-    <version>${revision}</version>0
+    <version>${revision}</version>
 
     <packaging>pom</packaging>
     <name>${project.artifactId}</name>
@@ -19,7 +19,6 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <spring.boot.starter.parent>2.7.0</spring.boot.starter.parent>
         <spring.boot.version>2.7.0</spring.boot.version>
-        .
         <knife4j.version>3.0.3</knife4j.version>
         <spring-web.version>5.3.32</spring-web.version>
         <jsoup.version>1.18.3</jsoup.version>

+ 5 - 0
java/storlead-framework/storlead-common/src/main/java/com/storlead/framework/common/constant/DSConstants.java

@@ -21,6 +21,11 @@ public class DSConstants {
      */
     public static final String DATASOURCE_SALES = "sales";
 
+    /**
+     * 营销/交易库(与 spring.datasource.dynamic.datasource.trade 的 key 一致)
+     */
+    public static final String DATASOURCE_TRADE = "trade";
+
     /**
      * 邮件库(与 spring.datasource.dynamic.datasource.mail 的 key 一致)
      */

+ 153 - 10
java/storlead-knowledge/storlead-knowledge-api/src/main/java/com/storlead/knowledge/api/ChunkController.java

@@ -6,6 +6,7 @@ import com.storlead.framework.common.result.Result;
 import com.storlead.knowledge.config.DifyProperties;
 import com.storlead.knowledge.pojo.dto.ChunkDTO;
 import com.storlead.knowledge.pojo.dto.QueryPageDTO;
+import com.storlead.knowledge.pojo.vo.KnowledgeStatisticsVO;
 import com.storlead.knowledge.utils.HttpService;
 import com.storlead.knowledge.utils.JacksonHolder;
 import io.swagger.annotations.Api;
@@ -13,6 +14,9 @@ import io.swagger.annotations.ApiOperation;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 
@@ -55,11 +59,15 @@ public class ChunkController {
     @ApiOperation("删除文本块")
     public Result<Object> deleteDataset(@PathVariable String dataset_id , @PathVariable String document_id , @PathVariable String segment_id ) {
         String url = difyProperties.getBaseUrl()  + "datasets/"+ dataset_id +"/documents/"+document_id +"/segments/"+segment_id;
-        return httpService.delete(
+        Result<Object> result = httpService.delete(
                 url,
                 null,
                 "Bearer "+difyProperties.getDatasetApiKey(),
                 new TypeReference<>() {}) ;
+        if (result.isSuccess()) {
+            return Result.result("删除成功");
+        }
+        return result;
     }
 
     @PostMapping("/{dataset_id}/{document_id}")
@@ -67,34 +75,169 @@ public class ChunkController {
     public Result<?> createSegments(
             @PathVariable String dataset_id,
             @PathVariable String document_id,
-            @RequestBody ChunkDTO request
-    ) throws JsonProcessingException {
-        String url = difyProperties.getBaseUrl()  + "datasets/"+ dataset_id +"/documents/"+document_id +"/segments/";
+            @RequestBody String  request
+    ) {
+        String url = difyProperties.getBaseUrl()  + "datasets/"+ dataset_id +"/documents/"+document_id +"/segments";
         return  httpService.post(
                 url,
-                Map.of(),
+                null,
                 "Bearer "+difyProperties.getDatasetApiKey(),
-                JacksonHolder.OBJECT_MAPPER.writeValueAsString(request),
+                request,
                 new TypeReference<>() {}
         );
-
     }
+
+
     @PostMapping("/{dataset_id}/{document_id}/{segment_id}")
     @ApiOperation("更新指定ID的文本块")
     public Result<Object> updateChunk(  @PathVariable String dataset_id,
                                         @PathVariable String document_id,
                                         @PathVariable String segment_id,
-                                        @RequestBody ChunkDTO request) throws JsonProcessingException {
+                                        @RequestBody String request) {
         String url = difyProperties.getBaseUrl()  + "datasets/"+ dataset_id +"/documents/"+document_id +"/segments/"+segment_id;
         return   httpService.post(
                 url,
-                Map.of(),
+                null,
                 "Bearer "+difyProperties.getDatasetApiKey(),
-                JacksonHolder.OBJECT_MAPPER.writeValueAsString(request),
+                request,
                 new TypeReference<>() {}
         );
+    }
+
+    @GetMapping("getAllChunks")
+    @ApiOperation("获取所有的文本块")
+    public Result<Object> getAllChunks(String dataset_id,QueryPageDTO page) {
+        // 1. 调用Dify API获取所有知识库列表(使用较大分页以获取全部数据)
+        String url = difyProperties.getBaseUrl() + "datasets";
+        Result<Object> datasetsResult = httpService.get(
+                url,
+                Map.of("page", 1, "limit", 100),
+                "Bearer " + difyProperties.getDatasetApiKey(),
+                new TypeReference<>() {}
+        );
+
+        List<Object> results =  new ArrayList<>();
+        if (datasetsResult.isSuccess()) {
+            Object responseData = datasetsResult.getResult();
+            if (responseData instanceof Map) {
+                Map<?, ?> responseMap = (Map<?, ?>) responseData;
+                // 遍历数据集,累加文档数和文本块数
+                Object dataObj = responseMap.get("data");
+                if (dataObj instanceof List) {
+                    List<?> dataList = (List<?>) dataObj;
+                    for (Object item : dataList) {
+                        if (item instanceof Map ) {
+                            Map<?, ?> datasetMap = (Map<?, ?>) item;
+                            String datasets_id = datasetMap.get("id").toString();
+                            if (dataset_id != null && !datasets_id.equals(dataset_id)) {
+                                continue;
+                            }
+                            results.addAll(getDataSetsChunks(datasets_id, page.getKeyword()));
+                        }
+                    }
+                }
+            }
+        }
+
+        // 对结果进行分页
+        int total = results.size();
+        int pageIndex = page.getPageIndex() != null ? page.getPageIndex() : 1;
+        int pageSize = page.getPageSize() != null ? page.getPageSize() : 10;
+        int fromIndex = (pageIndex - 1) * pageSize;
+        int toIndex = Math.min(fromIndex + pageSize, total);
+
+        List<Object> pagedResults;
+        if (fromIndex >= total) {
+            pagedResults = new ArrayList<>();
+        } else {
+            pagedResults = results.subList(fromIndex, toIndex);
+        }
+        Map<String,Object> resultMap = new HashMap<>() ;
+        resultMap.put("data", pagedResults);
+        resultMap.put("total", total);
+        resultMap.put("limit", pageSize);
+        resultMap.put("page", pageIndex);
+        resultMap.put("has_more", total > (fromIndex + pageSize));
+        return Result.ok(resultMap);
+    }
+
+
+    /**
+     * 获取单个知识库的所有的文本块
+     * @param dataset_id
+     * @return
+     */
+    public List<Object> getDataSetsChunks(String dataset_id,String keyword){
+        List<Object> results =  new ArrayList<>();
+        //获取知识库中文档的列表
+        String url1 = difyProperties.getBaseUrl() + "datasets/"+dataset_id+"/documents";
+        int page0 = 1;
+        while(true){
+             Result<Object> documents_list  = httpService.get(
+                url1,
+                Map.of("page", 1, "limit", 100),
+                "Bearer " + difyProperties.getDatasetApiKey(),
+                new TypeReference<>() {});
+
+            if (documents_list.isSuccess()) {
+                Object responseData1 = documents_list.getResult();
+                Map<?, ?> responseMap2 = (Map<?, ?>) responseData1;
+                Object dataObj1 = responseMap2.get("data");
+                //分页上线为100
+                Object has_more = responseMap2.get("has_more");
+
 
+                if (dataObj1 instanceof List) {
+                    List<?> dataList1 = (List<?>) dataObj1;
+                    for (Object item1 : dataList1) {
+                        if (item1 instanceof Map) {
+                            Map<?, ?> datasetMap1 = (Map<?, ?>) item1;
+                            String document_id = datasetMap1.get("id").toString();
+                            String url2 = difyProperties.getBaseUrl() + "datasets/" + dataset_id + "/documents/" + document_id+"/segments";
+                            int page = 1 ;
+                            while (true) {
+                                Result<Object> chunks_result = httpService.get(
+                                        url2,
+                                        keyword==null?Map.of("page", page, "limit", 100):
+                                                Map.of("page", page, "limit", 100,"keyword",keyword),
+                                        "Bearer " + difyProperties.getDatasetApiKey(),
+                                        new TypeReference<>() {
+                                        });
+                                if (chunks_result.isSuccess()) {
+                                    Object responseData2 = chunks_result.getResult();
+                                    Map<?, ?> responseMap3 = (Map<?, ?>) responseData2;
+                                    Object dataObj2 = responseMap3.get("data");
+                                    if (dataObj2 instanceof List) {
+                                        List<?> dataList2 = (List<?>) dataObj2;
+                                        //results.addAll(dataList2);
+                                        for (Object item2 : dataList2) {
+                                            @SuppressWarnings("unchecked")
+                                            Map<String, Object> datasetMap2 = (Map<String, Object>) item2;
+                                            datasetMap2.put("dataset_id", dataset_id);
+                                            results.add(datasetMap2);
+                                        }
+                                    }
+                                    //分页上线为100
+                                    Object has_more1 = responseMap3.get("has_more");
+                                    if (has_more1.toString().equals("false")) {
+                                        break;
+                                    }else{
+                                        page +=1 ;
+                                    }
+                                }
+                            }
 
+                        }
+                    }
+                }
+                if (has_more.toString().equals("false")) {
+                    break;
+                }else{
+                    page0 +=1 ;
+                }
+            }
+        }
+        return  results;
     }
 
 }

+ 119 - 0
java/storlead-knowledge/storlead-knowledge-api/src/main/java/com/storlead/knowledge/api/DocumentController.java

@@ -16,6 +16,9 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 
@@ -142,4 +145,120 @@ public class DocumentController {
         );
     }
 
+    @GetMapping("{dataset_id}/documents/{document_id}/download")
+    @ApiOperation("下载文档")
+    public Result<Object> downloadDocument (
+            @PathVariable String dataset_id,
+            @PathVariable String document_id
+    ) {
+        String url = difyProperties.getBaseUrl() + "datasets/"+dataset_id + "/documents/"+ document_id+"/download";
+        return httpService.get(
+                url,
+                null,
+                "Bearer "+difyProperties.getDatasetApiKey(),
+                new TypeReference<>() {});
+    }
+
+
+    @GetMapping("getAllDocumnets")
+    @ApiOperation("获取所有的文档")
+    public Result<Object> getAllDocumnets(String dataset_id,QueryPageDTO page) {
+        // 1. 调用Dify API获取所有知识库列表(使用较大分页以获取全部数据)
+        String url = difyProperties.getBaseUrl() + "datasets";
+        Result<Object> datasetsResult = httpService.get(
+                url,
+                Map.of("page", 1, "limit", 100),
+                "Bearer " + difyProperties.getDatasetApiKey(),
+                new TypeReference<>() {}
+        );
+
+        List<Object> results =  new ArrayList<>();
+        if (datasetsResult.isSuccess()) {
+            Object responseData = datasetsResult.getResult();
+            if (responseData instanceof Map) {
+                Map<?, ?> responseMap = (Map<?, ?>) responseData;
+                // 遍历数据集,累加文档数和文本块数
+                Object dataObj = responseMap.get("data");
+                if (dataObj instanceof List) {
+                    List<?> dataList = (List<?>) dataObj;
+                    for (Object item : dataList) {
+                        if (item instanceof Map ) {
+                            Map<?, ?> datasetMap = (Map<?, ?>) item;
+                            String datasets_id = datasetMap.get("id").toString();
+                            if (dataset_id != null && !datasets_id.equals(dataset_id)) {
+                                continue;
+                            }
+                            results.addAll(getDataSetsDocuments(datasets_id, page.getKeyword()));
+                        }
+                    }
+                }
+            }
+        }
+
+        // 对结果进行分页
+        int total = results.size();
+        int pageIndex = page.getPageIndex() != null ? page.getPageIndex() : 1;
+        int pageSize = page.getPageSize() != null ? page.getPageSize() : 10;
+        int fromIndex = (pageIndex - 1) * pageSize;
+        int toIndex = Math.min(fromIndex + pageSize, total);
+
+        List<Object> pagedResults;
+        if (fromIndex >= total) {
+            pagedResults = new ArrayList<>();
+        } else {
+            pagedResults = results.subList(fromIndex, toIndex);
+        }
+        Map<String,Object> resultMap = new HashMap<>() ;
+        resultMap.put("data", pagedResults);
+        resultMap.put("total", total);
+        resultMap.put("limit", pageSize);
+        resultMap.put("page", pageIndex);
+        resultMap.put("has_more", total > (fromIndex + pageSize));
+        return Result.ok(resultMap);
+    }
+
+
+    /**
+     * 获取单个知识库的所有的文本块
+     * @param dataset_id
+     * @return
+     */
+    public List<Object> getDataSetsDocuments(String dataset_id,String keyword){
+        List<Object> results =  new ArrayList<>();
+        //获取知识库中文档的列表
+        String url1 = difyProperties.getBaseUrl() + "datasets/"+dataset_id+"/documents";
+        int page0 = 1;
+        while(true){
+            Result<Object> documents_list  = httpService.get(
+                    url1,
+                    keyword==null?Map.of("page", 1, "limit", 100):Map.of("page", 1, "limit", 100,"keyword",keyword),
+                    "Bearer " + difyProperties.getDatasetApiKey(),
+                    new TypeReference<>() {});
+
+            if (documents_list.isSuccess()) {
+                Object responseData1 = documents_list.getResult();
+                Map<?, ?> responseMap2 = (Map<?, ?>) responseData1;
+                Object dataObj1 = responseMap2.get("data");
+                //分页上线为100
+                Object has_more = responseMap2.get("has_more");
+
+                if (dataObj1 instanceof List) {
+                    List<?> dataList1 = (List<?>) dataObj1;
+                    for (Object item2 : dataList1) {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> datasetMap2 = (Map<String, Object>) item2;
+                        datasetMap2.put("dataset_id", dataset_id);
+                        results.add(datasetMap2);
+                    }
+                }
+                if (has_more.toString().equals("false")) {
+                    break;
+                }else{
+                    page0 +=1 ;
+                }
+            }
+        }
+        return  results;
+    }
+
 }

+ 0 - 3
java/storlead-knowledge/storlead-knowledge-biz/src/main/java/com/storlead/knowledge/service/impl/ChatServiceImpl.java

@@ -11,8 +11,6 @@ import com.storlead.knowledge.service.AiMessageService;
 import com.storlead.knowledge.service.ChatService;
 import com.storlead.knowledge.utils.HttpService;
 import com.storlead.knowledge.utils.JacksonHolder;
-import lombok.extern.flogger.Flogger;
-import lombok.extern.log4j.Log4j2;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
@@ -23,7 +21,6 @@ import javax.annotation.Resource;
  * 对话服务实现
  */
 @Service
-@Log4j2
 public class ChatServiceImpl implements ChatService {
 
     private static final Logger log = LoggerFactory.getLogger(ChatServiceImpl.class);

+ 3 - 1
java/storlead-knowledge/storlead-knowledge-core/src/main/java/com/storlead/knowledge/utils/HttpService.java

@@ -58,7 +58,9 @@ public class HttpService {
     ) {
         int status = response.statusCode();
         String body = response.body();
-
+        if (status == 204) {
+            return (Result<T>) Result.result("删除成功");
+        }
         if (status >= 200 && status < 300) {
             try {
                 return Result.result(deserializer.apply(body));

+ 12 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/config/TradeSchedulingConfig.java

@@ -0,0 +1,12 @@
+package com.storlead.trade.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 启用 Spring 定时任务(无效客户分析投递等)
+ */
+@Configuration
+@EnableScheduling
+public class TradeSchedulingConfig {
+}

+ 25 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/constant/CustomerAnalysisConstants.java

@@ -0,0 +1,25 @@
+package com.storlead.trade.constant;
+
+/**
+ * 客户 AI 分析相关常量
+ */
+public final class CustomerAnalysisConstants {
+
+    private CustomerAnalysisConstants() {
+    }
+
+    /**
+     * 待分析(由其他业务消费后回写)
+     */
+    public static final int ANALYSIS_STATUS_PENDING = -1;
+
+    /**
+     * 无效客户(垃圾客户)标记
+     */
+    public static final int GARBAGE_CUSTOMER_YES = 1;
+
+    /**
+     * 同一客户再次投递间隔天数
+     */
+    public static final int NEXT_ANALYSIS_INTERVAL_DAYS = 15;
+}

+ 34 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/CustomerAiAnalysisController.java

@@ -3,6 +3,7 @@ package com.storlead.trade.controller;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.storlead.framework.common.constant.CommonConstant;
 import com.storlead.framework.common.result.Result;
 import com.storlead.trade.dto.CustomerAnalysisResultDTO;
 import com.storlead.trade.dto.CustomerSingleAnalysisRequestDTO;
@@ -19,7 +20,11 @@ import org.springframework.util.ObjectUtils;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.time.temporal.WeekFields;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 @RestController
@@ -141,4 +146,33 @@ public class CustomerAiAnalysisController {
         }
         return Result.result(statistics);
     }
+
+    @GetMapping("/customer-group/stats")
+    @ApiOperation("目标客户群统计数据")
+    public Result<Object> customerGroupStats() {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 全部潜客 - 查询总数(排除已删除)
+        LambdaQueryWrapper<CustomerAnalysisResultEntity> totalWrapper = new LambdaQueryWrapper<>();
+        totalWrapper.eq(CustomerAnalysisResultEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        long totalCustomers = customerAnalysisResultEntityService.count(totalWrapper);
+        result.put("totalCustomers", totalCustomers);
+        
+        // 本周新增 - 查询本周创建的数据(排除已删除)
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime weekStart = now.with(WeekFields.of(Locale.getDefault()).dayOfWeek(), 1).toLocalDate().atStartOfDay();
+        LambdaQueryWrapper<CustomerAnalysisResultEntity> weekWrapper = new LambdaQueryWrapper<>();
+        weekWrapper.eq(CustomerAnalysisResultEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                   .ge(CustomerAnalysisResultEntity::getCreateTime, weekStart);
+        long weeklyNew = customerAnalysisResultEntityService.count(weekWrapper);
+        result.put("weeklyNew", weeklyNew);
+        
+        // 高评分客户 - 默认值
+        result.put("highScoreCustomers", 124);
+        
+        // 沉默客户 - 默认值
+        result.put("silentCustomers", 89);
+        
+        return Result.result(result);
+    }
 }

+ 175 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/MarketingCampaignSopDetailController.java

@@ -0,0 +1,175 @@
+package com.storlead.trade.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.storlead.framework.common.result.Result;
+import com.storlead.trade.dto.MarketingCampaignSopDetailDTO;
+import com.storlead.trade.entity.MarketingCampaignSopDetailEntity;
+import com.storlead.trade.service.MarketingCampaignSopDetailService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.BeanUtils;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 营销活动SOP明细 Controller
+ */
+@RestController
+@RequestMapping("/marketing/campaign/sop-detail")
+@Api(tags = "营销: 营销活动SOP详情明细")
+public class MarketingCampaignSopDetailController {
+
+    @Resource
+    private MarketingCampaignSopDetailService marketingCampaignSopDetailService;
+
+    /**
+     * 根据id查询详情
+     */
+    @GetMapping("/get/{id}")
+    @ApiOperation(value = "根据id查询营销活动SOP明细详情", notes = "根据主键id查询详情")
+    public Result<Object> getById(@PathVariable Long id) {
+        MarketingCampaignSopDetailEntity entity = marketingCampaignSopDetailService.getById(id);
+        if (entity == null) {
+            return Result.error("数据不存在");
+        }
+        return Result.result(entity);
+    }
+
+    /**
+     * 分页查询
+     */
+    @PostMapping("/list")
+    @ApiOperation(value = "分页查询营销活动SOP明细", notes = "支持按营销活动id筛选")
+    public Result<Object> list(@RequestBody MarketingCampaignSopDetailDTO dto) {
+        Page<MarketingCampaignSopDetailEntity> page = new Page<>(dto.getPageIndex(), dto.getPageSize());
+        LambdaQueryWrapper<MarketingCampaignSopDetailEntity> wrapper = new LambdaQueryWrapper<>();
+        // 按营销活动id筛选
+        if (!ObjectUtils.isEmpty(dto.getMarketingCampaignId())) {
+            wrapper.eq(MarketingCampaignSopDetailEntity::getMarketingCampaignId, dto.getMarketingCampaignId());
+        }
+        // 按SOP明细id筛选
+        if (!ObjectUtils.isEmpty(dto.getSopDetailId())) {
+            wrapper.eq(MarketingCampaignSopDetailEntity::getSopDetailId, dto.getSopDetailId());
+        }
+        // 按步骤序号筛选
+        if (!ObjectUtils.isEmpty(dto.getStep())) {
+            wrapper.eq(MarketingCampaignSopDetailEntity::getStep, dto.getStep());
+        }
+        // 按触达渠道筛选
+        if (!ObjectUtils.isEmpty(dto.getType())) {
+            wrapper.eq(MarketingCampaignSopDetailEntity::getType, dto.getType());
+        }
+        // 按创建时间倒序
+        wrapper.orderByDesc(MarketingCampaignSopDetailEntity::getCreateTime);
+        // 分页查询
+        IPage<MarketingCampaignSopDetailEntity> iPage = marketingCampaignSopDetailService.page(page, wrapper);
+        return Result.result(iPage);
+    }
+
+    /**
+     * 新增
+     */
+    @PostMapping("/add")
+    @ApiOperation(value = "新增营销活动SOP明细", notes = "支持单条新增")
+    @Transactional(rollbackFor = Throwable.class)
+    public Result<?> add(@RequestBody MarketingCampaignSopDetailDTO dto) {
+        if (ObjectUtils.isEmpty(dto.getMarketingCampaignId())) {
+            return Result.error("营销活动id不能为空");
+        }
+        if (ObjectUtils.isEmpty(dto.getSopDetailId())) {
+            return Result.error("SOP明细id不能为空");
+        }
+        MarketingCampaignSopDetailEntity entity = new MarketingCampaignSopDetailEntity();
+        BeanUtils.copyProperties(dto, entity);
+        boolean isOk = marketingCampaignSopDetailService.save(entity);
+        if (!isOk) {
+            return Result.error("添加失败");
+        }
+        return Result.result(entity);
+    }
+
+    /**
+     * 批量新增
+     */
+    @PostMapping("/add/batch")
+    @ApiOperation(value = "批量新增营销活动SOP明细", notes = "支持批量新增")
+    @Transactional(rollbackFor = Throwable.class)
+    public Result<?> addBatch(@RequestBody List<MarketingCampaignSopDetailDTO> dtoList) {
+        if (CollectionUtils.isEmpty(dtoList)) {
+            return Result.error("数据不能为空");
+        }
+        List<MarketingCampaignSopDetailEntity> entityList = new ArrayList<>();
+        for (MarketingCampaignSopDetailDTO dto : dtoList) {
+            MarketingCampaignSopDetailEntity entity = new MarketingCampaignSopDetailEntity();
+            BeanUtils.copyProperties(dto, entity);
+            entityList.add(entity);
+        }
+        boolean isOk = marketingCampaignSopDetailService.saveBatch(entityList);
+        if (!isOk) {
+            return Result.error("批量添加失败");
+        }
+        return Result.ok();
+    }
+
+    /**
+     * 编辑
+     */
+    @PostMapping("/edit")
+    @ApiOperation(value = "编辑营销活动SOP明细", notes = "编辑营销活动SOP明细")
+    @Transactional(rollbackFor = Throwable.class)
+    public Result<?> edit(@RequestBody MarketingCampaignSopDetailDTO dto) {
+        if (ObjectUtils.isEmpty(dto.getId())) {
+            return Result.error("id不能为空");
+        }
+        MarketingCampaignSopDetailEntity entity = new MarketingCampaignSopDetailEntity();
+        BeanUtils.copyProperties(dto, entity);
+        boolean isOk = marketingCampaignSopDetailService.updateById(entity);
+        if (!isOk) {
+            return Result.error("修改失败");
+        }
+        return Result.result(entity);
+    }
+
+    /**
+     * 删除
+     */
+    @PostMapping("/delete")
+    @ApiOperation(value = "删除营销活动SOP明细", notes = "支持单条id或批量idList删除")
+    @Transactional(rollbackFor = Throwable.class)
+    public Result<?> delete(@RequestBody MarketingCampaignSopDetailDTO dto) {
+        boolean isOk = false;
+        Long id = dto.getId();
+        if (!ObjectUtils.isEmpty(id)) {
+            isOk = marketingCampaignSopDetailService.removeById(id);
+        }
+        List<Long> idList = dto.getIdList();
+        if (!CollectionUtils.isEmpty(idList)) {
+            isOk = marketingCampaignSopDetailService.removeByIds(idList);
+        }
+        if (!isOk) {
+            return Result.error("删除失败");
+        }
+        return Result.ok();
+    }
+
+    /**
+     * 根据营销活动id查询SOP明细列表
+     */
+    @GetMapping("/campaign/{marketingCampaignId}")
+    @ApiOperation(value = "根据营销活动id查询SOP明细列表", notes = "查询该营销活动关联的所有SOP明细")
+    public Result<Object> getByCampaignId(@PathVariable Long marketingCampaignId) {
+        LambdaQueryWrapper<MarketingCampaignSopDetailEntity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MarketingCampaignSopDetailEntity::getMarketingCampaignId, marketingCampaignId);
+        wrapper.orderByAsc(MarketingCampaignSopDetailEntity::getStep);
+        List<MarketingCampaignSopDetailEntity> list = marketingCampaignSopDetailService.list(wrapper);
+        return Result.result(list);
+    }
+}

+ 14 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/MarketingController.java

@@ -6,6 +6,7 @@ import com.storlead.framework.common.result.Result;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
@@ -66,4 +67,17 @@ public class MarketingController {
         return Result.ok();
     }
 
+    @GetMapping("/dashboard/stats")
+    @ApiOperation("营销: 仪表盘统计数据")
+    public Result<Object> dashboardStats() {
+        return Result.result(new java.util.HashMap<String, Object>() {{
+            put("monthlySent", "2.8K");
+            put("avgOpenRate", "45%");
+            put("clickRate", "12%");
+            put("replyRate", "8%");
+            put("protectedCustomers", 156);
+            put("pendingReview", 23);
+        }});
+    }
+
 }

+ 3 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketingCampaignDTO.java

@@ -116,6 +116,9 @@ public class MarketingCampaignDTO extends Page {
     @TableField("sop_id")
     private Integer sopId;
 
+    @ApiModelProperty(value = "SOP序列详情idList")
+    private List<Long> sopDetailIdList;
+
     @ApiModelProperty(value = "未打开邮件跟进规则(1是0否)")
     @TableField("follow_up_rules_not_opened")
     private Integer followUpRulesNotOpened;

+ 51 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketingCampaignSopDetailDTO.java

@@ -0,0 +1,51 @@
+package com.storlead.trade.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 营销活动SOP明细 DTO
+ */
+@Data
+@ApiModel(value = "MarketingCampaignSopDetailDTO", description = "营销活动SOP明细请求")
+public class MarketingCampaignSopDetailDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键id(编辑时必填)")
+    private Long id;
+
+    @ApiModelProperty(value = "主键id(编辑时必填)")
+    private List<Long> idList;
+
+    @ApiModelProperty(value = "营销活动id")
+    private Long marketingCampaignId;
+
+    @ApiModelProperty(value = "SOP明细id")
+    private Long sopDetailId;
+
+    @ApiModelProperty(value = "步骤序号")
+    private Integer step;
+
+    @ApiModelProperty(value = "步骤名称")
+    private String name;
+
+    @ApiModelProperty(value = "邮件内容模板")
+    private String emailContent;
+
+    @ApiModelProperty(value = "间隔天数")
+    private String days;
+
+    @ApiModelProperty(value = "触达渠道")
+    private String type;
+
+    @ApiModelProperty(value = "页码", required = true)
+    private Integer pageIndex = 1;
+
+    @ApiModelProperty(value = "每页数量", required = true)
+    private Integer pageSize = 10;
+}

+ 5 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/SopDetailCreateDTO.java

@@ -1,5 +1,6 @@
 package com.storlead.trade.dto;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -21,6 +22,10 @@ public class SopDetailCreateDTO implements Serializable {
     @ApiModelProperty(value = "步骤名称", required = true)
     private String name;
 
+    @ApiModelProperty(value = "邮件内容模板")
+    @TableField("email_content")
+    private String emailContent;
+
     @ApiModelProperty(value = "间隔天数")
     private String days;
 

+ 5 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/SopDetailUpdateDTO.java

@@ -1,5 +1,6 @@
 package com.storlead.trade.dto;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -21,6 +22,10 @@ public class SopDetailUpdateDTO implements Serializable {
 
     @ApiModelProperty(value = "步骤名称")
     private String name;
+    
+    @ApiModelProperty(value = "邮件内容模板")
+    @TableField("email_content")
+    private String emailContent;
 
     @ApiModelProperty(value = "间隔天数")
     private String days;

+ 3 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/CustomerEntity.java

@@ -97,4 +97,7 @@ public class CustomerEntity extends SysBaseField {
     private Date giveUpTime;
     @ApiModelProperty(value = "前拥有者")
     private Long formerOwnerBy;
+
+    @ApiModelProperty(value = "下次分析日期(按天)")
+    private Date nextAnalysisDate;
 }

+ 4 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/MarketingCampaignEntity.java

@@ -114,6 +114,10 @@ public class MarketingCampaignEntity extends SysBaseField {
     @TableField("sop_id")
     private Integer sopId;
 
+    @ApiModelProperty(value = "sop详情id(逗号分隔)")
+    @TableField("sop_detail_ids")
+    private String sopDetailIds;
+
     @ApiModelProperty(value = "未打开邮件跟进规则(1是0否)")
     @TableField("follow_up_rules_not_opened")
     private Integer followUpRulesNotOpened;

+ 55 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/MarketingCampaignSopDetailEntity.java

@@ -0,0 +1,55 @@
+package com.storlead.trade.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.storlead.framework.mybatis.entity.SysBaseField;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 营销活动SOP明细关联表 Entity
+ */
+@Data
+@TableName("marketing_campaign_sop_detail")
+@ApiModel(value = "MarketingCampaignSopDetailEntity", description = "营销活动SOP明细关联")
+public class MarketingCampaignSopDetailEntity extends SysBaseField implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键id")
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value = "营销活动id")
+    @TableField("marketing_campaign_id")
+    private Long marketingCampaignId;
+
+    @ApiModelProperty(value = "SOP明细id")
+    @TableField("sop_detail_id")
+    private Long sopDetailId;
+
+    @ApiModelProperty(value = "步骤序号")
+    @TableField("step")
+    private Integer step;
+
+    @ApiModelProperty(value = "步骤名称")
+    @TableField("name")
+    private String name;
+
+    @ApiModelProperty(value = "邮件内容模板")
+    @TableField("email_content")
+    private String emailContent;
+
+    @ApiModelProperty(value = "间隔天数")
+    @TableField("days")
+    private String days;
+
+    @ApiModelProperty(value = "触达渠道")
+    @TableField("type")
+    private String type;
+}

+ 4 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/SopDetailEntity.java

@@ -28,6 +28,10 @@ public class SopDetailEntity extends SysBaseField {
     @TableField("name")
     private String name;
 
+    @ApiModelProperty(value = "邮件内容模板")
+    @TableField("email_content")
+    private String emailContent;
+
     @ApiModelProperty(value = "间隔天数")
     @TableField("days")
     private String days;

+ 12 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/mapper/MarketingCampaignSopDetailMapper.java

@@ -0,0 +1,12 @@
+package com.storlead.trade.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.storlead.trade.entity.MarketingCampaignSopDetailEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 营销活动SOP明细 Mapper接口
+ */
+@Mapper
+public interface MarketingCampaignSopDetailMapper extends BaseMapper<MarketingCampaignSopDetailEntity> {
+}

+ 14 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/CustomerAnalysisDispatchService.java

@@ -0,0 +1,14 @@
+package com.storlead.trade.service;
+
+/**
+ * 无效客户分析投递:仅写入待分析记录,不做实际分析
+ */
+public interface CustomerAnalysisDispatchService {
+
+    /**
+     * 捞取到期的无效客户,插入 CustomerAnalysisResult(status=-1),并回写下次分析日期(+15天)
+     *
+     * @return 本次成功投递条数
+     */
+    int dispatchPendingInvalidCustomers();
+}

+ 1 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/MarketingCampaignEntityService.java

@@ -12,4 +12,5 @@ public interface MarketingCampaignEntityService extends MyBaseService<MarketingC
     Result<Object> addMarket(MarketingCampaignDTO marketingCampaignDTO);
 
     Result<Object> editMarket(MarketingCampaignDTO marketingCampaignDTO);
+    
 }

+ 10 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/MarketingCampaignSopDetailService.java

@@ -0,0 +1,10 @@
+package com.storlead.trade.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.storlead.trade.entity.MarketingCampaignSopDetailEntity;
+
+/**
+ * 营销活动SOP明细 Service接口
+ */
+public interface MarketingCampaignSopDetailService extends IService<MarketingCampaignSopDetailEntity> {
+}

+ 104 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/CustomerAnalysisDispatchServiceImpl.java

@@ -0,0 +1,104 @@
+package com.storlead.trade.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.storlead.framework.common.constant.CommonConstant;
+import com.storlead.trade.constant.CustomerAnalysisConstants;
+import com.storlead.trade.entity.CustomerAnalysisResultEntity;
+import com.storlead.trade.entity.CustomerEntity;
+import com.storlead.trade.service.CustomerAnalysisDispatchService;
+import com.storlead.trade.service.CustomerAnalysisResultEntityService;
+import com.storlead.trade.service.CustomerEntityService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class CustomerAnalysisDispatchServiceImpl implements CustomerAnalysisDispatchService {
+
+    private static final DateTimeFormatter BATCH_NO_FMT = DateTimeFormatter.BASIC_ISO_DATE;
+
+    @Resource
+    private CustomerEntityService customerEntityService;
+
+    @Resource
+    private CustomerAnalysisResultEntityService customerAnalysisResultEntityService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int dispatchPendingInvalidCustomers() {
+        LocalDate today = LocalDate.now();
+        String batchNo = today.format(BATCH_NO_FMT);
+        Date todayDate = java.sql.Date.valueOf(today);
+        Date nextAnalysisDate = java.sql.Date.valueOf(today.plusDays(CustomerAnalysisConstants.NEXT_ANALYSIS_INTERVAL_DAYS));
+
+        List<CustomerEntity> candidates = customerEntityService.list(new LambdaQueryWrapper<CustomerEntity>()
+                .eq(CustomerEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                .eq(CustomerEntity::getHasGarbageCustomer, CustomerAnalysisConstants.GARBAGE_CUSTOMER_YES)
+                .and(w -> w.isNull(CustomerEntity::getNextAnalysisDate)
+                        .or()
+                        .le(CustomerEntity::getNextAnalysisDate, todayDate)));
+
+        if (CollectionUtils.isEmpty(candidates)) {
+            log.info("无效客户分析投递:无待投递数据, batchNo={}", batchNo);
+            return 0;
+        }
+
+        Set<Long> alreadyDispatchedToday = loadDispatchedCustomerIds(batchNo);
+        List<CustomerAnalysisResultEntity> toInsert = new ArrayList<>();
+        List<CustomerEntity> toUpdate = new ArrayList<>();
+
+        for (CustomerEntity customer : candidates) {
+            if (customer.getId() == null || alreadyDispatchedToday.contains(customer.getId())) {
+                continue;
+            }
+            CustomerAnalysisResultEntity result = new CustomerAnalysisResultEntity();
+            result.setCustomerId(customer.getId());
+            result.setAnalysisBatchNo(batchNo);
+            result.setAnalysisStatus(CustomerAnalysisConstants.ANALYSIS_STATUS_PENDING);
+            result.setIsDelete(CommonConstant.DEL_FLAG_0);
+            toInsert.add(result);
+
+            CustomerEntity update = new CustomerEntity();
+            update.setId(customer.getId());
+            update.setNextAnalysisDate(nextAnalysisDate);
+            toUpdate.add(update);
+        }
+
+        if (CollectionUtils.isEmpty(toInsert)) {
+            log.info("无效客户分析投递:候选已全部在本批次投递过, batchNo={}, candidates={}", batchNo, candidates.size());
+            return 0;
+        }
+
+        customerAnalysisResultEntityService.saveBatch(toInsert);
+        customerEntityService.updateBatchById(toUpdate);
+        log.info("无效客户分析投递完成: batchNo={}, count={}, nextAnalysisDate={}", batchNo, toInsert.size(), nextAnalysisDate);
+        return toInsert.size();
+    }
+
+    private Set<Long> loadDispatchedCustomerIds(String batchNo) {
+        List<CustomerAnalysisResultEntity> exists = customerAnalysisResultEntityService.list(
+                new LambdaQueryWrapper<CustomerAnalysisResultEntity>()
+                        .select(CustomerAnalysisResultEntity::getCustomerId)
+                        .eq(CustomerAnalysisResultEntity::getAnalysisBatchNo, batchNo)
+                        .eq(CustomerAnalysisResultEntity::getIsDelete, CommonConstant.DEL_FLAG_0));
+        if (CollectionUtils.isEmpty(exists)) {
+            return new HashSet<>();
+        }
+        return exists.stream()
+                .map(CustomerAnalysisResultEntity::getCustomerId)
+                .filter(id -> id != null)
+                .collect(Collectors.toCollection(HashSet::new));
+    }
+}

+ 69 - 6
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignEntityServiceImpl.java

@@ -5,14 +5,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.storlead.framework.common.util.StringUtil;
+import com.storlead.trade.controller.MarketingCampaignSopDetailController;
 import com.storlead.trade.dto.MarketingCampaignDTO;
-import com.storlead.trade.entity.CustomerAnalysisResultEntity;
-import com.storlead.trade.entity.CustomerBaseEntity;
-import com.storlead.trade.entity.MarketingCampaignEntity;
+import com.storlead.trade.entity.*;
 import com.storlead.trade.mapper.MarketingCampaignEntityMapper;
-import com.storlead.trade.service.CustomerAnalysisResultEntityService;
-import com.storlead.trade.service.CustomerBaseEntityService;
-import com.storlead.trade.service.MarketingCampaignEntityService;
+import com.storlead.trade.service.*;
 import com.storlead.trade.vo.MarketingCampaignVO;
 import com.storlead.framework.common.constant.CommonConstant;
 import com.storlead.framework.common.result.Result;
@@ -20,6 +18,7 @@ import com.storlead.framework.mybatis.service.impl.MyBaseServiceImpl;
 import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
 
 
 import javax.annotation.Resource;
@@ -36,6 +35,10 @@ public class MarketingCampaignEntityServiceImpl
     private CustomerBaseEntityService customerBaseEntityService;
     @Resource
     private CustomerAnalysisResultEntityService customerAnalysisResultEntityService;
+    @Resource
+    private SopDetailService sopDetailService;
+    @Resource
+    private MarketingCampaignSopDetailService marketingCampaignSopDetailService;
     @Override
     public Result<Object> getList(MarketingCampaignDTO marketingCampaignDTO) {
         IPage<MarketingCampaignVO> pageVO = new Page<>(marketingCampaignDTO.getPageIndex(), marketingCampaignDTO.getPageSize());
@@ -112,6 +115,36 @@ public class MarketingCampaignEntityServiceImpl
                 return Result.error("添加失败");
             }
         }
+        //添加关联sopDetailIdList,并调取ai生成邮件内容
+        List<Long> sopDetailIdList = marketingCampaignDTO.getSopDetailIdList();
+        if (!CollectionUtils.isEmpty(sopDetailIdList)) {
+            //将sopDetailIdList转为 逗号分隔的字符串
+            String sopDetailIdListString = StringUtil.longListToString(sopDetailIdList);
+            //判断是否和原有的相同
+            if (!StringUtils.isEmpty(marketingCampaignEntity.getSopDetailIds()) && !marketingCampaignEntity.getSopDetailIds().equals(sopDetailIdListString)) {
+
+                marketingCampaignEntity.setSopDetailIds(sopDetailIdListString);
+                this.updateById(marketingCampaignEntity);
+                //删除原有关联sopDetail
+                LambdaUpdateWrapper<MarketingCampaignSopDetailEntity> MarketingCampaignSopDetailUpdateWrapper = new LambdaUpdateWrapper<>();
+                MarketingCampaignSopDetailUpdateWrapper.eq(MarketingCampaignSopDetailEntity::getMarketingCampaignId, marketingCampaignEntity.getId());
+                MarketingCampaignSopDetailUpdateWrapper.set(MarketingCampaignSopDetailEntity::getIsDelete, 1);
+                marketingCampaignSopDetailService.update(MarketingCampaignSopDetailUpdateWrapper);
+
+                List<SopDetailEntity> sopDetailEntityList = sopDetailService.listByIds(sopDetailIdList).stream().toList();
+                List<MarketingCampaignSopDetailEntity> marketingCampaignSopDetailEntityList = new ArrayList<>();
+                for (SopDetailEntity sopDetailEntity : sopDetailEntityList) {
+                    MarketingCampaignSopDetailEntity marketingCampaignSopDetailEntity = new MarketingCampaignSopDetailEntity();
+                    marketingCampaignSopDetailEntity.setMarketingCampaignId(marketingCampaignEntity.getId());
+                    marketingCampaignSopDetailEntity.setSopDetailId(sopDetailEntity.getId());
+                    //预留补充ai生成的邮件内容模板
+//                    marketingCampaignSopDetailEntity.setEmailContent();
+                    marketingCampaignSopDetailEntityList.add(marketingCampaignSopDetailEntity);
+                }
+                boolean saveOk = marketingCampaignSopDetailService.saveBatch(marketingCampaignSopDetailEntityList);
+            }
+
+        }
         return Result.ok();
     }
 
@@ -141,6 +174,36 @@ public class MarketingCampaignEntityServiceImpl
                 return Result.error("修改失败");
             }
         }
+        //添加关联sopDetailIdList,并调取ai生成邮件内容
+        List<Long> sopDetailIdList = marketingCampaignDTO.getSopDetailIdList();
+        if (!CollectionUtils.isEmpty(sopDetailIdList)) {
+            //将sopDetailIdList转为 逗号分隔的字符串
+            String sopDetailIdListString = StringUtil.longListToString(sopDetailIdList);
+            //判断是否和原有的相同
+            if (!StringUtils.isEmpty(marketingCampaignEntity.getSopDetailIds()) && !marketingCampaignEntity.getSopDetailIds().equals(sopDetailIdListString)) {
+
+                marketingCampaignEntity.setSopDetailIds(sopDetailIdListString);
+                this.updateById(marketingCampaignEntity);
+                //删除原有关联sopDetail
+                LambdaUpdateWrapper<MarketingCampaignSopDetailEntity> MarketingCampaignSopDetailUpdateWrapper = new LambdaUpdateWrapper<>();
+                MarketingCampaignSopDetailUpdateWrapper.eq(MarketingCampaignSopDetailEntity::getMarketingCampaignId, marketingCampaignEntity.getId());
+                MarketingCampaignSopDetailUpdateWrapper.set(MarketingCampaignSopDetailEntity::getIsDelete, 1);
+                marketingCampaignSopDetailService.update(MarketingCampaignSopDetailUpdateWrapper);
+
+                List<SopDetailEntity> sopDetailEntityList = sopDetailService.listByIds(sopDetailIdList).stream().toList();
+                List<MarketingCampaignSopDetailEntity> marketingCampaignSopDetailEntityList = new ArrayList<>();
+                for (SopDetailEntity sopDetailEntity : sopDetailEntityList) {
+                    MarketingCampaignSopDetailEntity marketingCampaignSopDetailEntity = new MarketingCampaignSopDetailEntity();
+                    marketingCampaignSopDetailEntity.setMarketingCampaignId(marketingCampaignEntity.getId());
+                    marketingCampaignSopDetailEntity.setSopDetailId(sopDetailEntity.getId());
+                    //预留补充ai生成的邮件内容模板
+//                    marketingCampaignSopDetailEntity.setEmailContent();
+                    marketingCampaignSopDetailEntityList.add(marketingCampaignSopDetailEntity);
+                }
+                boolean saveOk = marketingCampaignSopDetailService.saveBatch(marketingCampaignSopDetailEntityList);
+            }
+
+        }
         return Result.ok();
     }
 }

+ 16 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignSopDetailServiceImpl.java

@@ -0,0 +1,16 @@
+package com.storlead.trade.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.storlead.trade.entity.MarketingCampaignSopDetailEntity;
+import com.storlead.trade.mapper.MarketingCampaignSopDetailMapper;
+import com.storlead.trade.service.MarketingCampaignSopDetailService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 营销活动SOP明细 Service实现
+ */
+@Service
+public class MarketingCampaignSopDetailServiceImpl
+        extends ServiceImpl<MarketingCampaignSopDetailMapper, MarketingCampaignSopDetailEntity>
+        implements MarketingCampaignSopDetailService {
+}

+ 31 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/task/InvalidCustomerAnalysisDispatchTask.java

@@ -0,0 +1,31 @@
+package com.storlead.trade.task;
+
+import com.storlead.trade.service.CustomerAnalysisDispatchService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 每天 7 点:将到期的无效客户投递到 CustomerAnalysisResult(status=-1)
+ */
+@Slf4j
+@Component
+public class InvalidCustomerAnalysisDispatchTask {
+
+    @Resource
+    private CustomerAnalysisDispatchService customerAnalysisDispatchService;
+
+    @Scheduled(cron = "0 0 7 * * ?")
+    public void dispatch() {
+        long start = System.currentTimeMillis();
+        log.info("无效客户分析投递任务开始");
+//        try {
+//            int count = customerAnalysisDispatchService.dispatchPendingInvalidCustomers();
+//            log.info("无效客户分析投递任务结束, count={}, cost={}ms", count, System.currentTimeMillis() - start);
+//        } catch (Exception e) {
+//            log.error("无效客户分析投递任务失败", e);
+//        }
+    }
+}

+ 10 - 0
java/storlead-sasa/storlead-trade/src/main/resources/sql/customer_add_next_analysis_date.sql

@@ -0,0 +1,10 @@
+-- trade 库:客户表增加下次分析日期(按天);新建默认当天,存量可空(任务会按「空或已到期」捞取)
+ALTER TABLE `customer`
+  ADD COLUMN `next_analysis_date` date DEFAULT NULL COMMENT '下次分析日期' AFTER `former_owner_by`;
+
+-- 无效客户(垃圾客户)初始化为当天,便于首次被定时任务捞取
+UPDATE `customer`
+SET `next_analysis_date` = CURDATE()
+WHERE `is_delete` = 0
+  AND `has_garbage_customer` = 1
+  AND `next_analysis_date` IS NULL;

+ 2 - 0
ui/smarttrade-platform/.env.development

@@ -1,2 +1,4 @@
 # 开发环境后台接口地址
 VITE_API_BASE_URL=http://localhost:10010/
+
+VITE_API_APP_ID=7

+ 2 - 0
ui/smarttrade-platform/.env.production

@@ -1,2 +1,4 @@
 # 生产环境后台接口地址
 VITE_API_BASE_URL=/api
+
+VITE_API_APP_ID=7

+ 9 - 0
ui/smarttrade-platform/package-lock.json

@@ -11,6 +11,7 @@
         "@fortawesome/fontawesome-free": "^6.5.1",
         "axios": "^1.6.7",
         "element-plus": "^2.14.2",
+        "moment": "^2.30.1",
         "pinia": "^2.1.7",
         "vue": "^3.4.21",
         "vue-router": "^4.3.0"
@@ -1513,6 +1514,14 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/moment": {
+      "version": "2.30.1",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

+ 1 - 0
ui/smarttrade-platform/package.json

@@ -12,6 +12,7 @@
     "@fortawesome/fontawesome-free": "^6.5.1",
     "axios": "^1.6.7",
     "element-plus": "^2.14.2",
+    "moment": "^2.30.1",
     "pinia": "^2.1.7",
     "vue": "^3.4.21",
     "vue-router": "^4.3.0"

+ 31 - 1
ui/smarttrade-platform/src/api/knowledge/index.js

@@ -17,4 +17,34 @@ export const baseTraining = (params = {}) => get('/router/rest/knowledge/base/tr
 
 export const creatTraining = (data = {}) => post('/router/rest/knowledge/base/training', data)
 
-export const baseStatistics = (params = {}) => get('/router/rest/knowledge/base/statistics', params)
+export const baseStatistics = (params = {}) => get('/router/rest/knowledge/base/statistics', params)
+
+export const getAllChunks = (params = {}) => get('/router/rest/knowledge/chunk/getAllChunks', params)
+
+export const getAllDocumnets = (params = {}) => {
+    const query = { ...params }
+    if ([undefined, null, ''].includes(query.dataset_id)) {
+        delete query.dataset_id
+    }
+    return get('/router/rest/knowledge/document/getAllDocumnets', query)
+}
+
+export const delDocuments = (params = {}) => {
+    const { dataset_id, document_id, ...query } = params
+    return remove(`/router/rest/knowledge/document/${dataset_id}/documents/${document_id}`, params)
+}
+
+export const createChunk = (data = {}) => {
+    const { dataset_id,document_id, ...query } = data
+    return post(`/router/rest/knowledge/chunk/${dataset_id}/${document_id}`, data)
+}
+
+export const delChunk = (params = {}) => {
+    const { dataset_id, document_id,segment_id, ...query } = params
+    return  remove(`/router/rest/knowledge/chunk/${dataset_id}/${document_id}/${segment_id}`, params)
+}
+
+export const editChunk = (data = {}) => {
+    const { dataset_id,document_id,segment_id, ...query } = data
+    return post(`/router/rest/knowledge/chunk/${dataset_id}/${document_id}/${segment_id}`, data)
+}

+ 6 - 1
ui/smarttrade-platform/src/api/layout/index.js

@@ -1,6 +1,11 @@
 import { get, post, put, remove, upload } from '@/utils/request'
 
-export const getNoReadCount = () => post('/router/rest/sys/message/getNoReadCountByApp', {appId: 7})
+export const getNoReadCount = () => post('/router/rest/sys/message/getNoReadCount', {appId: import.meta.env.VITE_API_APP_ID})
+export const getMessageList = (data) => post('/router/rest/sys/message/pageList', data)
+export const updateRead = (data) => post('/router/rest/sys/message/read', data)
+export const updateReadAll = (data) => post('/router/rest/sys/message/readAll', data)
+export const listPageBuUserSet = (data) => post('/router/rest/sys/message/config/listPage', data)
+export const setMessageConfig = (data) => post('/router/rest/sys/message/config/setMessageConfig', data)
 export const getUserInfo = () => post('/router/rest/sys/auth/getCurrentUserInfo')
 export const updateUserPwd = (data) => post('/router/rest/sys/auth/modifyPass', data)
 export const updateRemark = (data) => post('/router/rest/sys/auth/updateRemark', data)

+ 2 - 0
ui/smarttrade-platform/src/assets/styles/main.css

@@ -1,4 +1,6 @@
 :root {
+  --el-color-primary: #6366f1;
+  
   /* 主色调 */
   --primary: #6366f1;
   --primary-light: #818cf8;

+ 13 - 3
ui/smarttrade-platform/src/components/layout/Sidebar.vue

@@ -330,6 +330,7 @@
 import { ref, onMounted } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { getUserInfo, updateUserPwd, getLoginData, updateRemark } from '@/api/layout'
+import { ElMessageBox } from 'element-plus'
 
 const route = useRoute()
 const router = useRouter()
@@ -380,9 +381,18 @@ const toggleTheme = () => {
 }
 
 const handleLogout = () => {
-  showUserMenu.value = false
-  // Handle logout
-  console.log('Logout')
+  ElMessageBox.confirm(
+    '确定要退出登录吗?',
+    '提示',
+    {
+      confirmButtonText: '确认',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  ).then(() => {
+    localStorage.clear()
+    router.push({ path: '/login' })
+  })
 }
 
 const pwdForm = ref({

+ 162 - 245
ui/smarttrade-platform/src/components/layout/TopBar.vue

@@ -28,7 +28,7 @@
       <!-- Notifications -->
       <div class="action-btn notification-btn" @click="toggleNotifications" :class="{ active: showNotifications }">
         <i class="fas fa-bell"></i>
-        <span v-if="unreadCount > 0" class="notification-badge">{{ unreadCount > 99 ? '99+' : unreadCount }}</span>
+        <span v-if="noReadCount > 0" class="notification-badge">{{ noReadCount > 99 ? '99+' : noReadCount }}</span>
       </div>
 
       <!-- Help -->
@@ -45,11 +45,11 @@
           <div class="panel-title">
             <i class="fas fa-bell"></i>
             消息中心
-            <span class="unread-count">{{ unreadCount }}条未读</span>
+            <span class="unread-count" v-if="noReadCount > 0">{{ noReadCount }}条未读</span>
           </div>
           <div class="panel-actions">
             <button class="action-text" @click="markAllAsRead">全部已读</button>
-            <button class="action-text" @click="showSettings = true">
+            <button class="action-text" @click="openSettings">
               <i class="fas fa-cog"></i>
             </button>
           </div>
@@ -59,14 +59,13 @@
         <div class="panel-tabs">
           <div 
             v-for="tab in notificationTabs" 
-            :key="tab.key"
+            :key="tab.messageType"
             class="panel-tab"
-            :class="{ active: activeTab === tab.key }"
-            @click="activeTab = tab.key"
+            :class="{ active: activeTab === tab.messageType }"
+            @click="activeTab = tab.messageType"
           >
-            <i :class="tab.icon"></i>
-            {{ tab.label }}
-            <span v-if="tab.count > 0" class="tab-count">{{ tab.count }}</span>
+            {{ tab.messageTypeName }}
+            <span v-if="tab.stateNumber > 0" class="tab-count">{{ tab.stateNumber > 99 ? '99+' : tab.stateNumber }}</span>
           </div>
         </div>
 
@@ -77,30 +76,23 @@
               v-for="notification in filteredNotifications" 
               :key="notification.id"
               class="notification-item"
-              :class="{ unread: !notification.read, [notification.type]: true }"
+              :class="{ unread: !notification.isRead }"
               @click="handleNotificationClick(notification)"
             >
-              <div class="notification-icon" :style="{ background: getIconBg(notification.type), color: getIconColor(notification.type) }">
-                <i :class="notification.icon"></i>
+              <div class="notification-icon" :style="{ background: getIconBg('reply'), color: getIconColor('reply') }">
+                <i class="fas fa-reply"></i>
               </div>
               <div class="notification-content">
                 <div class="notification-title">{{ notification.title }}</div>
-                <div class="notification-desc">{{ notification.description }}</div>
+                <div class="notification-desc">{{ notification.content }}</div>
                 <div class="notification-meta">
                   <span class="notification-time">{{ notification.time }}</span>
-                  <span v-if="notification.source" class="notification-source">
-                    <i :class="notification.sourceIcon"></i>
-                    {{ notification.source }}
-                  </span>
                 </div>
               </div>
               <div class="notification-actions">
-                <button v-if="!notification.read" class="mark-read-btn" @click.stop="markAsRead(notification.id)">
+                <button v-if="!notification.isRead" class="mark-read-btn" @click.stop="markAsRead(notification)">
                   <i class="fas fa-check"></i>
                 </button>
-                <button class="more-btn" @click.stop="showNotificationMenu($event, notification)">
-                  <i class="fas fa-ellipsis-v"></i>
-                </button>
               </div>
             </div>
           </template>
@@ -114,9 +106,7 @@
 
         <!-- Panel Footer -->
         <div class="panel-footer">
-          <button class="btn btn-secondary btn-sm" @click="viewAllNotifications">
-            <i class="fas fa-list"></i> 查看全部消息
-          </button>
+          <div></div>
           <button class="btn btn-primary btn-sm" @click="showNotifications = false">
             关闭
           </button>
@@ -224,8 +214,8 @@
         <div class="modal notification-detail-modal">
           <div class="modal-header">
             <div class="modal-title">
-              <div class="title-icon" :style="{ background: getIconBg(selectedNotification.type), color: getIconColor(selectedNotification.type) }">
-                <i :class="selectedNotification.icon"></i>
+              <div class="title-icon" :style="{ background: getIconBg('reply'), color: getIconColor('reply') }">
+                <i class="fas fa-reply"></i>
               </div>
               <span>{{ selectedNotification.title }}</span>
             </div>
@@ -238,7 +228,7 @@
               <i class="fas fa-clock"></i>
               {{ selectedNotification.time }}
             </div>
-            <div class="detail-content" v-html="selectedNotification.detail"></div>
+            <div class="detail-content" v-html="selectedNotification.content"></div>
             
             <!-- Quick Actions -->
             <div v-if="selectedNotification.actions" class="detail-actions">
@@ -580,27 +570,38 @@
   <AddMarketing ref="addMarketingRef" />
 
   <div v-if="showSettings" class="modal-overlay" @click.self="showSettings = false">
-    <div class="modal create-modal">
+    <div class="modal create-modal" style="max-width: 800px;">
       <div class="modal-header">
           <div class="modal-title"><i class="fas fa-bell"></i> 通知设置</div>
           <button class="modal-close" @click="showSettings = false"><i class="fas fa-times"></i></button>
       </div>
       <div class="modal-body">
         <div class="settings-list">
-          <div class="settings-item" style="padding-top: 0;">
-            <div class="item-icon" style="background: rgba(99, 102, 241, 0.2); color: var(--primary);">
+          <div class="settings-item" v-for="item in settingsData">
+            <!-- <div class="item-icon" style="background: rgba(99, 102, 241, 0.2); color: var(--primary);">
               <i class="fas fa-envelope"></i>
-            </div>
+            </div> -->
             <div class="item-content">
-              <div class="item-title">邮件通知</div>
-              <div class="item-desc">重要通知发送到邮箱</div>
+              <div class="item-title">{{ item.templateServiceName }}</div>
+              <div class="item-desc">{{ item.eventName }}</div>
             </div>
             <div class="item-action">
-              <el-switch v-model="settings.emailNotify" style="--el-switch-on-color: #13ce66;" />
+              <div>
+                <span>企微消息:</span>
+                <el-switch v-model="item.wecom" @change="changeSetting(item, 'wecom')" />
+              </div>
+              <div>
+                <span>站内信:</span>
+                <el-switch v-model="item.site" @change="changeSetting(item, 'site')" />
+              </div>
+              <div>
+                <span>邮件:</span>
+                <el-switch v-model="item.mail" @change="changeSetting(item, 'mail')" />
+              </div>
             </div>
           </div>
 
-          <div class="settings-item">
+          <!-- <div class="settings-item">
             <div class="item-icon" style="background: rgba(16, 185, 129, 0.2); color: var(--success);">
               <i class="fas fa-reply"></i>
             </div>
@@ -609,9 +610,20 @@
               <div class="item-desc">客户回复邮件、消息时即时提醒</div>
             </div>
             <div class="item-action">
-              <el-switch v-model="settings.replyNotify" style="--el-switch-on-color: #13ce66;" />
+              <div>
+                <span>企微消息:</span>
+                <el-switch v-model="settings.replyNotify" />
+              </div>
+              <div>
+                <span>站内信:</span>
+                <el-switch v-model="settings.replyNotify" />
+              </div>
+              <div>
+                <span>邮件:</span>
+                <el-switch v-model="settings.replyNotify" />
+              </div>
             </div>
-          </div>
+          </div> -->
         </div>
       </div>
     </div>
@@ -622,7 +634,8 @@
 import { ref, computed, onMounted } from 'vue'
 import { useRoute } from 'vue-router'
 import AddMarketing from '../addMarketing.vue'
-import { getNoReadCount } from '@/api/layout'
+import { getNoReadCount, getMessageList, updateRead, updateReadAll, listPageBuUserSet } from '@/api/layout'
+import moment from 'moment'
 
 const route = useRoute()
 const showQuickActions = ref(false)
@@ -770,9 +783,32 @@ const generateReport = () => {
   alert('报告生成中,完成后将发送至您的邮箱...')
 }
 
+// Notification data
+const notifications = ref([])
 const toggleNotifications = () => {
   showNotifications.value = !showNotifications.value
-  if (showNotifications.value) showHelp.value = false
+  if (showNotifications.value){
+    showHelp.value = false
+    let params = {
+      pageIndex: 1,
+      pageSize: 10000,
+      appId: import.meta.env.VITE_API_APP_ID
+    }
+    getMessageList(params).then(res => {
+      notifications.value = (res.result.records || []).map(item => {
+          // 判断创建时间是不是今天
+          let date = moment(item.createTime).format("YYYY-MM-DD HH:mm:ss")
+          if(date.slice(0, 10) == moment().format("YYYY-MM-DD")){
+              item.time = "今天 " + date.slice(11, 16)
+          }else{
+              item.time = moment(item.createTime).format("MM月DD日 HH:mm")
+          }
+          return item
+      })
+    }).catch(err => {
+      console.error('获取消息列表失败:', err)
+    })
+  }
 }
 
 const toggleHelp = () => {
@@ -780,196 +816,8 @@ const toggleHelp = () => {
   if (showHelp.value) showNotifications.value = false
 }
 
-// Notification data
-const notifications = ref([
-  // 客户回复
-  {
-    id: 1,
-    type: 'reply',
-    icon: 'fas fa-reply',
-    title: 'Riyadh Tech 回复了邮件',
-    description: '对方对您的产品报价表示感兴趣,希望进一步了解交期和付款方式',
-    detail: '<p>客户 Ahmed Al-Rashid 回复了您的报价邮件:</p><blockquote style="border-left:3px solid var(--primary);padding-left:12px;margin:12px 0;color:var(--text-secondary);">您好,我们对贵公司的SSD产品很感兴趣。请问MOQ是多少?交期大概多久?付款方式有哪些?</blockquote><p>建议尽快回复客户,可以提供详细的报价单和公司介绍。</p>',
-    time: '5分钟前',
-    source: '邮件',
-    sourceIcon: 'fas fa-envelope',
-    read: false,
-    actions: [
-      { label: '立即回复', icon: 'fas fa-reply', primary: true },
-      { label: '查看邮件', icon: 'fas fa-envelope-open' },
-    ]
-  },
-  {
-    id: 2,
-    type: 'reply',
-    icon: 'fab fa-whatsapp',
-    title: 'Dubai Storage WhatsApp 已读',
-    description: '您发送的消息已被查看,对方正在输入中...',
-    detail: '<p>客户 Mohammed Khan 已读您的 WhatsApp 消息,显示对方正在输入回复。</p><p>消息内容:关于企业级SSD采购的询价</p>',
-    time: '15分钟前',
-    source: 'WhatsApp',
-    sourceIcon: 'fab fa-whatsapp',
-    read: false
-  },
-  {
-    id: 3,
-    type: 'reply',
-    icon: 'fab fa-linkedin',
-    title: 'LinkedIn 好友请求被接受',
-    description: 'Kuwait Data Systems 的 CIO Faisal 接受了您的好友请求',
-    detail: '<p>Faisal Al-Mutairi (CTO @ Kuwait Data Systems) 接受了您的 LinkedIn 连接请求。</p><p>建议:发送个性化感谢消息,介绍公司和产品。</p>',
-    time: '1小时前',
-    source: 'LinkedIn',
-    sourceIcon: 'fab fa-linkedin',
-    read: true
-  },
-  
-  // 风险预警
-  {
-    id: 4,
-    type: 'risk',
-    icon: 'fas fa-exclamation-triangle',
-    title: '⚠️ 高风险预警:客户信用下调',
-    description: 'ABC Corp 信用评级从 A 下调至 B,建议提高预付款比例',
-    detail: '<p><strong>风险等级:高</strong></p><p>客户 ABC Corp 的信用评级发生变动:</p><ul><li>原评级:A</li><li>新评级:B</li><li>原因:近期财务状况恶化</li></ul><p><strong>建议措施:</strong></p><ul><li>提高预付款比例至 50%</li><li>缩短付款周期</li><li>关注后续订单风险</li></ul>',
-    time: '30分钟前',
-    source: '风控系统',
-    sourceIcon: 'fas fa-shield-alt',
-    read: false,
-    actions: [
-      { label: '查看详情', icon: 'fas fa-info-circle', primary: true },
-      { label: '调整付款条件', icon: 'fas fa-edit' }
-    ]
-  },
-  {
-    id: 5,
-    type: 'risk',
-    icon: 'fas fa-gavel',
-    title: '欧盟能效新规即将生效',
-    description: '2026年7月生效,涉及SSD产品能效认证要求',
-    detail: '<p><strong>法规更新提醒</strong></p><p>欧盟委员会发布新的能效认证要求,将于2026年7月1日正式生效。</p><p><strong>影响范围:</strong></p><ul><li>所有出口欧盟的SSD产品</li><li>需要提供能效测试报告</li><li>认证周期约30天</li></ul><p><strong>建议:</strong>提前准备相关认证材料。</p>',
-    time: '2小时前',
-    source: '市场情报',
-    sourceIcon: 'fas fa-globe',
-    read: false
-  },
-  
-  // AI情报
-  {
-    id: 6,
-    type: 'ai',
-    icon: 'fas fa-robot',
-    title: '🤖 AI发现新商机',
-    description: '识别到新加坡 Digital Hub 近期频繁访问官网定价页面',
-    detail: '<p><strong>AI商机分析报告</strong></p><p>客户行为分析:</p><ul><li>过去7天访问定价页面 12 次</li><li>查看企业级SSD产品详情 8 次</li><li>下载产品手册 2 次</li></ul><p><strong>AI建议:</strong>该客户意向度高,建议尽快主动联系。</p>',
-    time: '45分钟前',
-    source: 'AI大脑',
-    sourceIcon: 'fas fa-brain',
-    read: false,
-    actions: [
-      { label: '立即联系', icon: 'fas fa-phone', primary: true },
-      { label: '发送报价', icon: 'fas fa-file-invoice' }
-    ]
-  },
-  {
-    id: 7,
-    type: 'ai',
-    icon: 'fas fa-lightbulb',
-    title: '💡 AI策略建议',
-    description: '基于近期数据分析,建议调整中东地区邮件发送时间',
-    detail: '<p><strong>策略优化建议</strong></p><p>AI分析了过去30天的邮件数据,发现:</p><ul><li>中东客户最佳回复时段:晚上8-10点</li><li>当前发送时段:上午10点</li><li>预计调整后回复率提升:52%</li></ul><p>是否应用此策略?</p>',
-    time: '3小时前',
-    source: 'AI策略中心',
-    sourceIcon: 'fas fa-lightbulb',
-    read: true,
-    actions: [
-      { label: '应用策略', icon: 'fas fa-check', primary: true },
-      { label: '查看详情', icon: 'fas fa-chart-bar' }
-    ]
-  },
-  
-  // 营销报告
-  {
-    id: 8,
-    type: 'marketing',
-    icon: 'fas fa-chart-line',
-    title: '📊 周度营销报告已生成',
-    description: '本周发送邮件 1,247 封,打开率 42%,回复率 18%',
-    detail: '<p><strong>本周营销数据汇总</strong></p><table style="width:100%;border-collapse:collapse;margin:12px 0;"><tr style="border-bottom:1px solid var(--border)"><td style="padding:8px 0;">邮件发送</td><td style="text-align:right;font-weight:600;">1,247 封</td></tr><tr style="border-bottom:1px solid var(--border)"><td style="padding:8px 0;">打开率</td><td style="text-align:right;font-weight:600;color:var(--success)">42% (+8%)</td></tr><tr style="border-bottom:1px solid var(--border)"><td style="padding:8px 0;">回复率</td><td style="text-align:right;font-weight:600;color:var(--success)">18% (+5%)</td></tr><tr><td style="padding:8px 0;">新增商机</td><td style="text-align:right;font-weight:600;">23 个</td></tr></table><p>详细报告已发送至您的邮箱。</p>',
-    time: '今天 09:00',
-    source: '自动报告',
-    sourceIcon: 'fas fa-file-alt',
-    read: true,
-    actions: [
-      { label: '查看完整报告', icon: 'fas fa-file-pdf', primary: true }
-    ]
-  },
-  {
-    id: 9,
-    type: 'marketing',
-    icon: 'fas fa-check-circle',
-    title: '✅ 营销活动完成',
-    description: '中东潜客开发活动已完成,共触达 156 位客户',
-    detail: '<p><strong>活动执行报告</strong></p><p>活动名称:中东潜客开发</p><p>执行时间:2026-03-12 ~ 2026-03-19</p><p><strong>执行结果:</strong></p><ul><li>目标客户:156 位</li><li>邮件送达:152 封 (97%)</li><li>邮件打开:68 封 (45%)</li><li>客户回复:12 封 (8%)</li><li>意向客户:5 位</li></ul>',
-    time: '昨天 18:30',
-    source: '营销系统',
-    sourceIcon: 'fas fa-paper-plane',
-    read: true
-  },
-  
-  // 寻客通知
-  {
-    id: 10,
-    type: 'prospect',
-    icon: 'fas fa-user-plus',
-    title: '🎯 发现 12 个高匹配潜客',
-    description: '基于您的历史成交客户,AI 推荐 12 个相似潜客',
-    detail: '<p><strong>AI寻客报告</strong></p><p>寻客条件:中东地区 + SSD进口商</p><p><strong>推荐潜客 TOP 3:</strong></p><ol><li>Riyadh Tech Distribution (92分)</li><li>Dubai Storage Solutions (88分)</li><li>Saudi Digital Hub (76分)</li></ol><p>点击查看完整列表。</p>',
-    time: '今天 14:30',
-    source: '自动寻客',
-    sourceIcon: 'fas fa-crosshairs',
-    read: false,
-    actions: [
-      { label: '查看潜客列表', icon: 'fas fa-list', primary: true },
-      { label: '发送营销邮件', icon: 'fas fa-paper-plane' }
-    ]
-  },
-  {
-    id: 11,
-    type: 'prospect',
-    icon: 'fas fa-search',
-    title: '寻客任务更新',
-    description: '欧洲企业级存储寻客任务进度 75%,已发现 45 家目标公司',
-    detail: '<p><strong>任务进度更新</strong></p><p>任务名称:欧洲企业级存储寻客</p><p>当前进度:75%</p><p><strong>已发现:</strong></p><ul><li>目标公司:45 家</li><li>关键人信息:28 位</li><li>高匹配度:12 家</li></ul>',
-    time: '今天 11:20',
-    source: '寻客任务',
-    sourceIcon: 'fas fa-tasks',
-    read: true
-  },
-  
-  // 系统通知
-  {
-    id: 12,
-    type: 'system',
-    icon: 'fas fa-cog',
-    title: '系统更新通知',
-    description: '领存智贸 AI 已更新至 v2.3,新增策略自进化功能',
-    detail: '<p><strong>版本更新 v2.3</strong></p><p><strong>新增功能:</strong></p><ul><li>AI策略自进化 - 自动测试优化营销策略</li><li>智能跟进提醒 - 基于客户行为预测最佳跟进时机</li><li>知识库问答优化 - 支持更多行业知识</li></ul><p><strong>优化:</strong></p><ul><li>邮件生成速度提升 30%</li><li>潜客匹配准确率提升 15%</li></ul>',
-    time: '昨天 10:00',
-    source: '系统',
-    sourceIcon: 'fas fa-server',
-    read: true
-  }
-])
-
 // Computed
-const notificationTabs = computed(() => [
-  { key: 'all', label: '全部', icon: 'fas fa-inbox', count: notifications.value.filter(n => !n.read).length },
-  { key: 'reply', label: '客户回复', icon: 'fas fa-reply', count: notifications.value.filter(n => n.type === 'reply' && !n.read).length },
-  { key: 'marketing', label: '营销报告', icon: 'fas fa-chart-line', count: notifications.value.filter(n => n.type === 'marketing' && !n.read).length }
-])
-
-const unreadCount = computed(() => notifications.value.filter(n => !n.read).length)
+const notificationTabs = ref([])
 
 const filteredNotifications = computed(() => {
   if (activeTab.value === 'all') return notifications.value
@@ -1002,30 +850,81 @@ const getIconColor = (type) => {
 }
 
 const handleNotificationClick = (notification) => {
-  if (!notification.read) {
-    notification.read = true
-  }
+  setRead(notification)
   selectedNotification.value = notification
+  // if(notification.callbackUrl){
+  //     window.open(item.callbackUrl, '_blank')
+  // }
 }
 
-const markAsRead = (id) => {
-  const notification = notifications.value.find(n => n.id === id)
-  if (notification) {
-    notification.read = true
-  }
+const markAsRead = (notification) => {
+  setRead(notification)
 }
 
-const markAllAsRead = () => {
-  notifications.value.forEach(n => n.read = true)
+const setRead = (item) => {
+  if(!item.isRead){
+      updateRead({ messageLogIds: [item.id], appId: import.meta.env.VITE_API_APP_ID }).then(res => {
+        item.isRead = 1
+        getCount()
+        // item.stateNumber --
+      }).catch(err => {
+        ElMessage.error(err.msg);
+      })
+  }
 }
 
-const viewAllNotifications = () => {
-  showNotifications.value = false
-  // Navigate to notifications page
+const markAllAsRead = () => {
+  if(notifications.value.length == 0){
+      ElMessage.error('列表暂无数据');
+      return
+  }
+  updateReadAll({ appId: import.meta.env.VITE_API_APP_ID }).then(res => {
+      if(res.success){
+          ElMessage.success('操作成功');
+          notifications.value.map(item => item.isRead = 1)
+          getCount()
+      }else{
+          ElMessage.error(res.msg);
+      }
+  })
 }
 
-const showNotificationMenu = (event, notification) => {
-  // Show context menu
+const settingsData = ref([])
+const openSettings = () => {
+  listPageBuUserSet({ pageIndex: 1, pageSize: 9999, appId: import.meta.env.VITE_API_APP_ID }).then(res => {
+    settingsData.value = res.result.records.map(item => {
+      item.wecom = handleData('wecom', item)
+      item.site = handleData('site', item)
+      item.mail = handleData('mail', item)
+      return item
+    })
+  })
+  showSettings.value = true
+}
+const handleData = (type, column) => {
+  let enabled = true
+	let templateData = null
+	if(column.userDetails && column.userDetails.length > 0) {
+		templateData = column.userDetails.find(item => item.templateEventId == column.id && item.templateDetailType == type)
+      //找到对应的配置就按照对应的配置显示
+      if(templateData) {
+          enabled = templateData?.enabled
+      }
+  }
+  return enabled
+}
+const changeSetting = (item, type) => {
+  let params = {
+    appId: import.meta.env.VITE_API_APP_ID,
+    templateDetailType: type,
+    templateEventId: item.id,
+    enabled: item[type]
+  }
+  setMessageConfig(params).then(res => {
+    ElMessage.success((item[type] ? '启用' : '禁用') + '成功')
+  }).catch(err => {
+    ElMessage.error(err.msg)
+  })
 }
 
 const handleAction = (action) => {
@@ -1106,10 +1005,19 @@ const showPrivacy = () => {
   console.log('Show privacy')
 }
 
-onMounted(() => {
+const noReadCount = ref(0)
+const getCount = () => {
   getNoReadCount().then(res => {
-    console.log('Unread notifications count:', res)
+    let countList = (res.result || []).map(item => item.stateNumber)
+    noReadCount.value = eval(countList.join("+"))
+    notificationTabs.value = [
+      { messageType: 'all', messageTypeName: '全部', stateNumber: noReadCount.value },
+      ...res.result
+    ]
   })
+}
+onMounted(() => {
+  getCount()
 })
 </script>
 
@@ -1701,6 +1609,7 @@ onMounted(() => {
   position: sticky;
   top: 0;
   z-index: 10;
+  margin-bottom: 0;
 }
 
 .panel-title {
@@ -2364,6 +2273,14 @@ onMounted(() => {
   cursor: pointer;
 }
 
+.settings-item:first-child{
+  padding-top: 0;
+}
+
+.settings-item .el-switch{
+  --el-switch-on-color: #13ce66;
+}
+
 .item-icon {
   width: 40px;
   height: 40px;

+ 20 - 4
ui/smarttrade-platform/src/views/CustomersView.vue

@@ -69,7 +69,7 @@
     </div>
 
     <!-- Customer List -->
-    <div style="flex: 1; overflow-y: auto;">
+    <!-- <div style="flex: 1; overflow-y: auto;"> -->
       <div v-for="customer in filteredCustomers" :key="customer.id" 
            class="customer-card"
            @click="showCustomerDetail(customer)">
@@ -83,11 +83,11 @@
             </span> -->
           </div>
           <div class="customer-contact">
-            <i class="fas fa-user"></i> {{ customer.liaison.name }}<span v-if="customer.liaison.departmentPosition"> · </span>{{ customer.liaison.departmentPosition }}
+            <i class="fas fa-user"></i> {{ customer.liaison?.name }}<span v-if="customer.liaison?.departmentPosition"> · </span>{{ customer.liaison?.departmentPosition }}
           </div>
           <div class="customer-meta">
             <span><i class="fas fa-map-marker-alt"></i> {{ customer.continent }} · {{ customer.country }}</span>
-            <span><i class="fas fa-industry"></i> {{ customer.customerCompany.industry }}</span>
+            <span><i class="fas fa-industry"></i> {{ customer.customerCompany?.industry }}</span>
             <!-- <span><i class="fas fa-dollar-sign"></i> {{ customer.annualPurchase }}</span> -->
           </div>
         </div>
@@ -99,7 +99,18 @@
           <button class="btn btn-secondary btn-sm" @click="showCustomerDetail(customer)"><i class="fas fa-eye"></i>更多详情</button>
         </div>
       </div>
-    </div>
+      <div class="pagination-area">
+        <el-pagination
+          background
+          :total="customerStats.total"
+          style="text-align: right;"
+          :current-page="params.pageIndex"
+          :page-size="params.pageSize"
+          layout="total, prev, pager, next"
+          @current-change="pageCurrentChange">
+        </el-pagination>
+      </div>
+    <!-- </div> -->
 
     <!-- 引入添加客户弹窗组件 -->
     <!-- <AddCustomerModal 
@@ -279,6 +290,7 @@ const tabs = ref([
   // { key: 'silent', label: '沉默客户', count: 55 }
 ])
 const customers = ref([])
+const customerTotal = ref()
 const params = ref({
   pageIndex: 1, 
   pageSize: 10,
@@ -293,6 +305,10 @@ const getCustomer = () => {
   })
 }
 getCustomer()
+const pageCurrentChange = (page) => {
+    params.value.pageIndex = page
+    getCustomer()
+}
 
 const regionValue = ref([])
 const getRegion = (value) => {

+ 375 - 81
ui/smarttrade-platform/src/views/KnowledgeView.vue

@@ -6,9 +6,9 @@
         <p class="page-desc">AI 驱动的企业知识管理,智能问答与知识沉淀</p>
       </div>
       <div class="header-actions">
-        <button class="btn btn-secondary" @click="showImportModal = true">
+        <!-- <button class="btn btn-secondary" @click="showImportModal = true">
           <i class="fas fa-file-import"></i> 批量导入
-        </button>
+        </button> -->
         <button class="btn btn-primary" @click="addKnowledgeDocument()">
           <i class="fas fa-plus"></i> 添加知识
         </button>
@@ -109,9 +109,13 @@
         </div>
       </div>
       <div class="tab-actions">
+        <select class="form-select" v-if="activeTab=='all'" v-model="categoryId" @change="changeCategory" style="width: 250px;">
+          <option value="">选择分类</option>
+          <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
+        </select>
         <div class="search-box">
           <i class="fas fa-search search-icon"></i>
-          <input type="text" v-model="searchQuery" class="search-input" placeholder="搜索知识...">
+          <input type="text" v-model="searchQuery" class="search-input" @change="searchData" placeholder="搜索知识...">
         </div>
       </div>
     </div>
@@ -153,11 +157,14 @@
       <div class="pagination-area">
         <el-pagination
           background
-          layout="prev, pager, next"
-          :total="1000">
+          :total="categoriesTotal"
+          style="text-align: right;"
+          :current-page="knowledgeParams.pageIndex"
+          :page-size="knowledgeParams.pageSize"
+          layout="total, prev, pager, next"
+          @current-change="knowledgeCurrentChange">
         </el-pagination>
       </div>
-      
     </div>
 
     <!-- 全部知识 Tab -->
@@ -165,7 +172,7 @@
       <div class="knowledge-list">
         <div v-for="item in knowledgeList" :key="item.id"
              class="knowledge-card"
-             @click="showKnowledgeDetail(item)">
+             @click="showDocumentDetail(item)">
           <!-- <div class="knowledge-icon" :style="{ background: item.iconBg }">{{ item.icon }}</div> -->
           <div class="knowledge-content">
             <div class="knowledge-header">
@@ -180,15 +187,31 @@
             </div> -->
           </div>
           <div class="knowledge-actions" @click.stop>
-            <button class="btn btn-secondary btn-sm" @click="editKnowledge(item)">
+            <button class="btn btn-secondary btn-sm" @click="editDocument(item)">
               <i class="fas fa-edit"></i>
             </button>
-            <button class="btn btn-secondary btn-sm" @click="deleteKnowledge(item)">
-              <i class="fas fa-trash"></i>
-            </button>
+            <el-popconfirm title="确认删除?" @confirm="deleteKnowledge(item)">
+              <template #reference>
+                <button class="btn btn-secondary btn-sm">
+                  <i class="fas fa-trash"></i>
+                </button>
+              </template>
+            </el-popconfirm>
+            
           </div>
         </div>
       </div>
+      <div class="pagination-area">
+          <el-pagination
+            background
+            :total="categoryTotal"
+            style="text-align: right;"
+            :current-page="documentsParams.pageIndex"
+            :page-size="documentsParams.pageSize"
+            layout="total, prev, pager, next"
+            @current-change="categoryPageCurrentChange">
+          </el-pagination>
+        </div>
     </div>
 
     <!-- 问答记录 Tab -->
@@ -237,6 +260,17 @@
             </div>
           </div>
         </div>
+        <div class="pagination-area">
+          <el-pagination
+            background
+            :total="qaRecordsTotal"
+            style="text-align: right;"
+            :current-page="chatParams.pageIndex"
+            :page-size="chatParams.pageSize"
+            layout="total, prev, pager, next"
+            @current-change="chatPageCurrentChange">
+          </el-pagination>
+        </div>
       </div>
     </div>
 
@@ -279,6 +313,18 @@
             </div>
           </div>
         </div>
+        <div class="pagination-area">
+          <el-pagination
+            background
+            :total="trainingTasksTotal"
+            style="text-align: right;"
+            :current-page="trainingParams.pageIndex"
+            :page-size="trainingParams.pageSize"
+            layout="total, prev, pager, next"
+            @current-change="tasksPageCurrentChange">
+          </el-pagination>
+        </div>
+        
       </div>
     </div>
 
@@ -354,28 +400,35 @@
       </div>
     </div>
 
-    <!-- Add Knowledge Modal -->
-    <div v-if="showAddModal" class="modal-overlay" @click.self="showAddModal = false">
+    <!-- 添加知识弹窗 -->
+    <div v-if="showAddModal" class="modal-overlay" style="z-index: 1002;" @click.self="showAddModal = false">
       <div class="modal add-modal">
         <div class="modal-header">
-          <div class="modal-title"><i class="fas fa-plus"></i> 添加知识</div>
+          <div class="modal-title"><i class="fas fa-plus"></i> {{chunkType=='add'?'添加':'编辑'}}知识</div>
           <button class="modal-close" @click="showAddModal = false"><i class="fas fa-times"></i></button>
         </div>
         <div class="modal-body">
           <div class="form-group">
             <label class="form-label">问题/关键词 <span class="required">*</span></label>
-            <input type="text" class="form-input" v-model="newKnowledge.name" placeholder="例如:MOQ最小起订量是多少?">
+            <input type="text" class="form-input" v-model="newKnowledge.content" placeholder="例如:MOQ最小起订量是多少?">
           </div>
           <div class="form-group">
             <label class="form-label">答案/内容 <span class="required">*</span></label>
-            <textarea class="form-textarea" v-model="newKnowledge.text" rows="4" placeholder="输入详细答案..."></textarea>
+            <textarea class="form-textarea" v-model="newKnowledge.answer" rows="4" placeholder="输入详细答案..."></textarea>
           </div>
           <div class="form-group">
             <label class="form-label">知识分类 <span class="required">*</span></label>
-            <select class="form-select" v-model="newKnowledge.dataset_id">
+            <select class="form-select" v-model="newKnowledge.dataset_id" @change='getDocument' :disabled='chunkType=="edit"'>
               <option value="">选择分类</option>
               <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
             </select>
+          </div>
+          <div class="form-group">
+            <label class="form-label">知识文档 <span class="required">*</span></label>
+            <select class="form-select" v-model="newKnowledge.document_id" :disabled='chunkType=="edit"'>
+              <option value="">选择文档</option>
+              <option v-for="cat in documentList" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
+            </select>
           </div>
             <!-- <div class="form-group">
               <label class="form-label">关键词标签</label>
@@ -388,20 +441,78 @@
         </div>
         <div class="modal-footer">
           <button class="btn btn-secondary" @click="showAddModal = false">取消</button>
-          <button class="btn btn-primary" @click="addKnowledge"><i class="fas fa-plus"></i> 添加</button>
+          <button class="btn btn-primary" @click="addKnowledge"><i class="fas fa-plus"></i> {{chunkType=='add'?'添加':'确认'}}</button>
         </div>
       </div>
     </div>
 
-    <!-- Knowledge Detail Modal -->
+    <!-- 知识文本块列表弹窗 -->
+    <div v-if="showChunksModal" class="modal-overlay" @click.self="showChunksModal = false">
+      <div class="modal import-modal" style="min-width: 1000px;">
+        <div class="modal-header">
+          <div class="modal-title"><i class="fas fa-file-import"></i> {{ documentName }}</div>
+          <button class="modal-close" @click="showChunksModal = false"><i class="fas fa-times"></i></button>
+        </div>
+        <div class="modal-body">
+          <div class="knowledge-list" v-loading="chunkLoading" element-loading-background="rgba(0, 0, 0, 0.8)">
+            <div v-for="item in chunkList" :key="item.id"
+                class="knowledge-card"
+                @click="showKnowledgeDetail(item)">
+              <!-- <div class="knowledge-icon" :style="{ background: item.iconBg }">{{ item.icon }}</div> -->
+              <div class="knowledge-content">
+                <div class="knowledge-header">
+                  <div class="knowledge-question">{{ item.sign_content }}</div>
+                  <!-- <span class="knowledge-category" :style="{ background: item.iconBg }">{{ item.categoryName }}</span> -->
+                </div>
+                <div class="knowledge-answer">{{ item.answer }}</div>
+                <!-- <div class="knowledge-meta">
+                  <span><i class="fas fa-eye"></i> {{ item.views }}次查看</span>
+                  <span><i class="fas fa-thumbs-up"></i> {{ item.likes }}次好评</span>
+                  <span><i class="fas fa-clock"></i> {{ item.time }}</span>
+                </div> -->
+              </div>
+              <div class="knowledge-actions" @click.stop>
+                <button class="btn btn-secondary btn-sm" @click="editKnowledge(item)">
+                  <i class="fas fa-edit"></i>
+                </button>
+                <el-popconfirm title="确认删除?" @confirm="deletechunk(item)">
+                  <template #reference>
+                    <button class="btn btn-secondary btn-sm">
+                      <i class="fas fa-trash"></i>
+                    </button>
+                  </template>
+                </el-popconfirm>
+                
+              </div>
+            </div>
+          </div>
+          <div class="pagination-area">
+            <el-pagination
+              background
+              :total="chunksTotal"
+              style="text-align: right;padding: 0px;"
+              :current-page="chunkParams.pageIndex"
+              :page-size="chunkParams.pageSize"
+              layout="total, prev, pager, next"
+              @current-change="chunkCurrentChange">
+            </el-pagination>
+          </div>
+        </div>
+        <div class="modal-footer">
+          <button class="btn btn-secondary" @click="showChunksModal = false">关闭</button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 知识块详情弹窗 -->
     <div v-if="showDetailModal && selectedKnowledge" class="modal-overlay" @click.self="showDetailModal = false">
       <div class="modal detail-modal">
         <div class="modal-header">
           <div class="modal-title">
-            <span class="detail-icon" :style="{ background: selectedKnowledge.iconBg }">{{ selectedKnowledge.icon }}</span>
+            <!-- <span class="detail-icon" :style="{ background: selectedKnowledge.iconBg }">{{ selectedKnowledge.icon }}</span> -->
             <div>
-              <h3>{{ selectedKnowledge.question }}</h3>
-              <p>{{ selectedKnowledge.categoryName }}</p>
+              <h3>{{ selectedKnowledge.sign_content }}</h3>
+              <!-- <p>{{ selectedKnowledge.categoryName }}</p> -->
             </div>
           </div>
           <button class="modal-close" @click="showDetailModal = false"><i class="fas fa-times"></i></button>
@@ -411,22 +522,22 @@
             <div class="detail-label"><i class="fas fa-lightbulb"></i> 答案内容</div>
             <div class="detail-content">{{ selectedKnowledge.answer }}</div>
           </div>
-          <div class="detail-section" v-if="selectedKnowledge.extendedAnswer">
+          <!-- <div class="detail-section" v-if="selectedKnowledge.extendedAnswer">
             <div class="detail-label"><i class="fas fa-file-alt"></i> 扩展说明</div>
             <div class="detail-content extended">{{ selectedKnowledge.extendedAnswer }}</div>
-          </div>
-          <div class="detail-section">
+          </div> -->
+          <!-- <div class="detail-section">
             <div class="detail-label"><i class="fas fa-tags"></i> 关键词标签</div>
             <div class="detail-tags">
               <span v-for="tag in selectedKnowledge.tags" :key="tag" class="tag">{{ tag }}</span>
             </div>
-          </div>
-          <div class="detail-section" v-if="selectedKnowledge.relatedQuestions && selectedKnowledge.relatedQuestions.length">
+          </div> -->
+          <!-- <div class="detail-section" v-if="selectedKnowledge.relatedQuestions && selectedKnowledge.relatedQuestions.length">
             <div class="detail-label"><i class="fas fa-link"></i> 相关问题</div>
             <div class="related-questions">
               <div v-for="(q, idx) in selectedKnowledge.relatedQuestions" :key="idx" class="related-item">{{ q }}</div>
             </div>
-          </div>
+          </div> -->
           <div class="detail-stats-row">
             <div class="detail-stat">
               <i class="fas fa-eye"></i>
@@ -515,6 +626,7 @@
           
           <div class="import-zone">
             <el-upload
+              ref="uploadRef"
               class="upload-demo"
               drag
               action=""
@@ -524,14 +636,14 @@
               multiple>
               <div class="import-icon"><i class="fas fa-cloud-upload-alt"></i></div>
               <div class="import-text">拖拽文件到此处,或点击上传</div>
-              <div class="import-formats">支持 Excel、CSV、JSON、PDF 格式</div>
+              <div class="import-formats">支持 Excel、CSV、JSON、PDF、TXT、DOC、MD 格式</div>
             </el-upload>
           </div>
           <div class="import-options">
             <div class="import-option" v-for="item in fileListRaw" :key="item.name">
               <i class="fas fa-file-excel"></i>
               <span>{{ item.name }}</span>
-              <button icon="el-icon-edit"></button>
+              <i class="fas fa-times" style="color: #fff;cursor: pointer;" @click="delFile(item)"></i>
             </div>
           </div>
         </div>
@@ -555,7 +667,7 @@
 
 <script setup>
 import { ref, computed } from 'vue'
-import { knowledgeDatasets,knowledgeDocuments,createKnowledge,chatMessages,baseTraining,creatTraining,baseStatistics } from '../api/knowledge'
+import { knowledgeDatasets,knowledgeDocuments,createKnowledge,createChunk,editChunk,chatMessages,baseTraining,creatTraining,baseStatistics,getAllChunks,getAllDocumnets,delDocuments,delChunk } from '../api/knowledge'
 import { formatTimeAgo } from '@/utils/time'
 import { ElMessage } from 'element-plus'
 
@@ -563,7 +675,10 @@ const showAddModal = ref(false)
 const showDetailModal = ref(false)
 const showImportModal = ref(false)
 const showTrainModal = ref(false)
+const showChunksModal = ref(false)
+const chunkLoading = ref(false)
 const selectedKnowledge = ref(null)
+const selectedDocument = ref(null)
 const activeTab = ref('categories')
 const searchQuery = ref('')
 const qaFilter = ref('all')
@@ -572,6 +687,7 @@ const confidenceThreshold = ref(70)
 const showToast = ref(false)
 const toastMessage = ref('')
 const toastType = ref('success')
+const chunkType = ref('add')
 
 
 const syncStatus = {
@@ -591,11 +707,9 @@ const tabs = ref([
 
 const newKnowledge = ref({
   dataset_id: '',
-  text: '',
-  name: '',
-  indexing_technique: "high_quality",
-  doc_form: "text_model",
-  doc_language:"Chinese Simplified"
+  document_id: '',
+  content: '',
+  answer: '',
 })
 
 // const categories = ref([
@@ -643,51 +757,166 @@ const getBaseStatistics = () => {
     baseStatistics().then(res => {
     knowledgeStats.value = res.result
     tabs.value[1].count = res.result.totalDocuments
-    console.log('res :>> ', res);
   })
 }
 getBaseStatistics()
 
 
 const changeTab = (tab)=>{
+  searchQuery.value = ''
+  categoryId.value = ''
   activeTab.value = tab 
-  if(tab =='qa'){
-    getChatMessages()
+  searchData()
+}
+
+const searchData = () => {
+  switch (activeTab.value) {
+    case 'categories':
+      getKnowledgeDatasets()
+      break
+    case 'all':
+      getCategoryList()
+      break
+    case 'qa':
+      getChatMessages()
+      break
+    case 'training':
+      getTraining()
+      break
+    default:
+      return '未知状态'
   }
 }
+
+
 const categories = ref([])
+const categoriesTotal = ref()
+const knowledgeParams=ref({
+  pageIndex: 1, 
+  pageSize: 10,
+  keyword:'',
+})
 // 获取知识分类
 const getKnowledgeDatasets = () => {
-    knowledgeDatasets({ pageIndex: 1, pageSize: 10 }).then(res => {
+    knowledgeParams.value.keyword = searchQuery.value
+    knowledgeDatasets(knowledgeParams.value).then(res => {
     categories.value = res.result.data
     res.result.data.forEach(el => {
       el.timeip = formatTimeAgo(el.updated_at)
     });
-    tabs.value[0].count = res.result.total
-    console.log('res :>> ', res);
+    tabs.value[0].count = categoriesTotal.value =  res.result.total
   })
 }
 getKnowledgeDatasets()
+const knowledgeCurrentChange = (page) => {
+    knowledgeParams.value.pageIndex = page
+    getKnowledgeDatasets()
+}
+
+const changeCategory = () => {
+  getCategoryList(categoryId.value)
+}
 
 const knowledgeList = ref([])
-// 获取知识分类下的知识
-const documentsParams = ref({})
+const documentList = ref([])
+// 获取知识列表
+const categoryId = ref()
+const categoryTotal = ref()
+const documentsParams = ref({
+  pageIndex:1,
+  pageSize:10,
+  // dataset_id:'',
+  keyword:''
+})
 const openCategory = (category) => {
   activeTab.value = 'all'
-  searchQuery.value = category.name
   showToastMessage(`正在加载 ${category.name} 分类...`, 'success')
-  documentsParams.value = {
-    pageIndex:1,
-    pageSize:10,
-    dataset_id:category.id
+  getCategoryList(category.id)
+}
+const getCategoryList  = (id,type) => {
+  if(!type){
+    categoryId.value = id?id:''
+    documentsParams.value.dataset_id = id?id:''
+    documentsParams.value.keyword = searchQuery.value
+    documentsParams.value.pageSize = 10
+  }else{
+    documentsParams.value.pageSize = 9999
+  }
+  getAllDocumnets(documentsParams.value).then(res => {
+    console.log(res)
+    if(type=='add'){ //添加知识获取分类
+      documentList.value = res.result.data
+    }else{
+      knowledgeList.value = res.result.data
+      tabs.value[1].count = categoryTotal.value = res.result.total
+    }
+  })
+}
+getCategoryList()
+const categoryPageCurrentChange = (page) => {
+    documentsParams.value.pageIndex = page
+    getCategoryList(categoryId.value)
+}
+
+// 删除知识文档
+const deleteKnowledge = (item) => {
+  let params = {
+    dataset_id:item.dataset_id,
+    document_id:item.id,
+  }
+  delDocuments(params).then(res => {
+    if(res.success){
+      showToastMessage('知识文档已删除', 'success')
+      getCategoryList()
+    }else{
+      ElMessage.error(res.message)
+    }
+  })
+}
+// 获取知识列表下的知识块
+const chunkList = ref([])
+const chunksTotal = ref()
+const chunkParams=ref({
+  pageIndex: 1, 
+  pageSize: 10,
+  dataset_id:'',
+  document_id:'',
+})
+// 获取知识分类
+const getChunkDatasets = () => {
+    chunkLoading.value = true
+    getAllChunks(chunkParams.value).then(res => {
+      chunkLoading.value = false
+      chunkList.value = res.result.data
+      res.result.data.forEach(el => {
+        el.timeip = formatTimeAgo(el.updated_at)
+      });
+      chunksTotal.value =  res.result.total
+    })
+}
+const chunkCurrentChange = (page) => {
+    chunkParams.value.pageIndex = page
+    getChunkDatasets()
+}
+// 删除知识块
+const deletechunk = (item) => {
+  let params = {
+    dataset_id:item.dataset_id,
+    document_id:item.document_id,
+    segment_id:item.id,
   }
-  knowledgeDocuments(documentsParams.value).then(res => {
-    knowledgeList.value = res.result.data
-    tabs.value[1].count = res.result.total
+  delChunk(params).then(res => {
+    if(res.success){
+      showToastMessage('知识块已删除', 'success')
+      getChunkDatasets()
+    }else{
+      ElMessage.error(res.message)
+    }
   })
 }
 
 const qaRecords = ref([])
+const qaRecordsTotal = ref()
 // 获取问答记录
 const chatParams=ref({
   pageIndex: 1, 
@@ -695,18 +924,24 @@ const chatParams=ref({
   keyword:'',
 })
 const getChatMessages = () => {
-    chatMessages(chatParams.value).then(res => {
-    tabs.value[2].count = res.result.total
+  chatParams.value.keyword = searchQuery.value
+  chatMessages(chatParams.value).then(res => {
+    tabs.value[2].count = qaRecordsTotal.value = res.result.total
     res.result.records.forEach(el => {
       el.time = formatTimeAgo(el.createdAt)
     });
     qaRecords.value = res.result.records
-    console.log('res :>> ', res);
   })
 }
 getChatMessages()
+const chatPageCurrentChange = (page) => {
+    chatParams.value.pageIndex = page
+    getChatMessages()
+}
+
 
 const trainingTasks = ref([])
+const trainingTasksTotal = ref()
 //获取ai训练列表
 const trainingParams=ref({
   pageIndex: 1, 
@@ -714,18 +949,29 @@ const trainingParams=ref({
   keyword:'',
 })
 const getTraining = () => {
+    trainingParams.value.keyword = searchQuery.value
     baseTraining(trainingParams.value).then(res => {
-    tabs.value[3].count = res.result.total
+    tabs.value[3].count = trainingTasksTotal.value = res.result.total
     trainingTasks.value = res.result.records
-    console.log('res :>> ', res);
   })
 }
 getTraining()
+const tasksPageCurrentChange = (page) => {
+    trainingParams.value.pageIndex = page
+    getTraining()
+}
 
+const uploadRef = ref(null)
 const fileListRaw = ref([])
+const removeUidSet = new Set()
 const handleFileChange = (file, fileList) => {
   // 提取所有原生File对象
-  fileListRaw.value = fileList.map(item => item.raw)
+  console.log(fileList)
+  // fileListRaw.value = fileList.map(item => item.raw)
+  const exist = fileListRaw.value.some(f => f.uid === file.uid)
+  if (!exist) {
+    fileListRaw.value.push(file)
+  }
   console.log('当前选中所有文件:', fileListRaw.value)
 }
 const trainKnowledge = ref({
@@ -734,8 +980,11 @@ const trainKnowledge = ref({
   description: '',
 })
 const startTrain = async () => {
+  if(trainKnowledge.value.datasetId==''||trainKnowledge.value.name==''||trainKnowledge.value.description==''){
+    return ElMessage.warning('请填写必填项')
+  }
   if (fileListRaw.value.length === 0) {
-    return ElMessage.warning('请先选择文件')
+    return ElMessage.warning('请选择文件')
   }
   const formData = new FormData()
   const jsonBody = {
@@ -746,20 +995,48 @@ const startTrain = async () => {
   formData.append('data', JSON.stringify(jsonBody))
   
   fileListRaw.value.forEach(file => {
-    formData.append('file', file)
+    formData.append('file', file.raw)
   })
 
   try {
     await creatTraining(formData)
-    ElMessage.success('全部文件上传成功')
+    ElMessage.success('添加成功')
     // 清空上传列表、缓存文件
     uploadRef.value.clearFiles()
     fileListRaw.value = []
+    trainKnowledge.value = {
+      datasetId: '',
+      name: '',
+      description: '',
+    }
+    showTrainModal.value = false
+    getTraining()
   } catch (err) {
-    ElMessage.error(err?.message || '上传失败,请重试')
+    ElMessage.error(err?.message || '上传失败,请重试')
   }
 }
 
+const delFile = (targetItem) => {
+  fileListRaw.value = fileListRaw.value.filter(item => item.uid !== targetItem.uid)
+}
+
+const documentName = ref()
+const showDocumentDetail = (item) => {
+  selectedDocument.value = item
+  showChunksModal.value = true
+  documentName.value = item.name
+  chunkParams.value.dataset_id = item.dataset_id
+  chunkParams.value.document_id = item.id
+  getChunkDatasets()
+}
+
+const showKnowledgeDetail = (item) => {
+  selectedKnowledge.value = item
+  showDetailModal.value = true
+}
+
+
+
 // const filteredCategories = computed(() => {
 //   if (!searchQuery.value) return categories.value
 //   return categories.value.filter(c => c.name.includes(searchQuery.value) || c.description.includes(searchQuery.value))
@@ -790,37 +1067,54 @@ const formatNumber = (num) => num.toLocaleString()
 
 
 
-const showKnowledgeDetail = (item) => {
-  selectedKnowledge.value = item
-  showDetailModal.value = true
-}
-
 const editKnowledge = (item) => {
-  showDetailModal.value = false
-  showToastMessage('正在编辑知识...', 'success')
+  chunkType.value = 'edit'
+  newKnowledge.value.content = item.content
+  newKnowledge.value.answer=item.answer
+  newKnowledge.value.dataset_id=item.dataset_id
+  newKnowledge.value.segment_id=item.id
+  getCategoryList(item.dataset_id,'add')
+  newKnowledge.value.document_id=item.document_id
+  showAddModal.value = true
+}
+const editDocument = (item) => {
+  
 }
 
-const deleteKnowledge = (item) => {
-  showToastMessage('知识已删除', 'success')
+const getDocument = () =>{
+  getCategoryList(newKnowledge.value.dataset_id,'add')
 }
 
 const addKnowledgeDocument = ()=> {
-  newKnowledge.value.name = ''
-  newKnowledge.value.text=''
+  newKnowledge.value.content = ''
+  newKnowledge.value.answer=''
   newKnowledge.value.dataset_id=''
+  newKnowledge.value.document_id=''
+  newKnowledge.value.segment_id=''
+  chunkType.value = 'add'
   showAddModal.value = true
 }
 const addKnowledge = () => {
-  if(!newKnowledge.value.name||!newKnowledge.value.text||!newKnowledge.value.dataset_id){
+  if(!newKnowledge.value.answer||!newKnowledge.value.content||!newKnowledge.value.dataset_id||!newKnowledge.value.document_id){
     ElMessage.error('请填入必填项')
     return false
   }
-  createKnowledge(newKnowledge.value).then(res => {
-    if(res.success){
-      howToastMessage('知识添加成功,AI 已学习', 'success')
-      showAddModal.value = false
-    }
-  })
+  if(chunkType.value =='add'){
+    createChunk(newKnowledge.value).then(res => {
+      if(res.success){
+        howToastMessage('知识添加成功,AI 已学习', 'success')
+        showAddModal.value = false
+      }
+    })
+  }else{
+    editChunk(newKnowledge.value).then(res => {
+      if(res.success){
+        howToastMessage('知识编辑成功,AI 已学习', 'success')
+        showAddModal.value = false
+      }
+    })
+  }
+  
 }
 
 const optimizeAnswer = (item) => {
@@ -929,7 +1223,7 @@ const showToastMessage = (message, type = 'success') => {
 .knowledge-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
 .knowledge-content { flex: 1; }
 .knowledge-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
-.knowledge-question { font-size: 14px; font-weight: 600; }
+.knowledge-question { font-size: 14px; font-weight: 600; word-break: break-all;}
 .knowledge-category { font-size: 10px; padding: 2px 8px; border-radius: 10px; }
 .knowledge-answer { font-size: 12px; color: var(--text-secondary); margin-bottom: 8px; line-height: 1.5; }
 .knowledge-meta { display: flex; gap: 16px; font-size: 11px; color: var(--text-muted); }