Quellcode durchsuchen

添加文档\图片\文本总结功能, 增加密码校验

zhenghao vor 5 Monaten
Ursprung
Commit
ce1b967835
30 geänderte Dateien mit 452 neuen und 335 gelöschten Zeilen
  1. 2 3
      pom.xml
  2. 3 3
      src/main/java/cn/jlsxwkj/common/R/ResponseError.java
  3. 1 0
      src/main/java/cn/jlsxwkj/common/config/SaTokenConfigure.java
  4. 19 0
      src/main/java/cn/jlsxwkj/common/config/UserConfig.java
  5. 11 0
      src/main/java/cn/jlsxwkj/common/exception/AccountPasswordCheckFailException.java
  6. 4 1
      src/main/java/cn/jlsxwkj/common/exception/InsertFailException.java
  7. 1 1
      src/main/java/cn/jlsxwkj/common/exception/LoginAccountAlreadyLoginException.java
  8. 1 1
      src/main/java/cn/jlsxwkj/common/exception/LoginWrongPasswordException.java
  9. 4 3
      src/main/java/cn/jlsxwkj/common/exception/UnknownException.java
  10. 1 1
      src/main/java/cn/jlsxwkj/common/filter/ExecTime.java
  11. 8 4
      src/main/java/cn/jlsxwkj/common/handler/GlobalRestExceptionHandler.java
  12. 0 1
      src/main/java/cn/jlsxwkj/common/handler/ResultResponseHandler.java
  13. 16 57
      src/main/java/cn/jlsxwkj/common/reader/ParagraphDocReader.java
  14. 49 0
      src/main/java/cn/jlsxwkj/common/reader/ParagraphOcrReader.java
  15. 13 19
      src/main/java/cn/jlsxwkj/common/reader/ParagraphTextReader.java
  16. 37 0
      src/main/java/cn/jlsxwkj/common/utils/CheckPassword.java
  17. 4 12
      src/main/java/cn/jlsxwkj/common/utils/MergeDocuments.java
  18. 7 6
      src/main/java/cn/jlsxwkj/common/utils/SplitDocument.java
  19. 34 29
      src/main/java/cn/jlsxwkj/moudles/chat/ChatController.java
  20. 194 150
      src/main/java/cn/jlsxwkj/moudles/chat/ChatService.java
  21. 3 3
      src/main/java/cn/jlsxwkj/moudles/chathistory/ChatHistory.java
  22. 12 12
      src/main/java/cn/jlsxwkj/moudles/chathistory/ChatHistoryMapper.java
  23. 2 4
      src/main/java/cn/jlsxwkj/moudles/chathistory/ChatHistoryService.java
  24. 4 3
      src/main/java/cn/jlsxwkj/moudles/logerror/LogErrorMapper.java
  25. 2 4
      src/main/java/cn/jlsxwkj/moudles/logerror/LogErrorService.java
  26. 1 2
      src/main/java/cn/jlsxwkj/moudles/userlist/UserListController.java
  27. 9 9
      src/main/java/cn/jlsxwkj/moudles/userlist/UserListMapper.java
  28. 5 4
      src/main/java/cn/jlsxwkj/moudles/userlist/UserListService.java
  29. 4 1
      src/main/resources/application-dev.yml
  30. 1 2
      src/main/resources/application.yml

+ 2 - 3
pom.xml

@@ -92,11 +92,10 @@
 			<artifactId>hutool-all</artifactId>
 			<version>5.8.15</version>
 		</dependency>
-		<!--apache commons-->
 		<dependency>
 			<groupId>org.apache.commons</groupId>
-			<artifactId>commons-lang3</artifactId>
-			<version>3.6</version>
+			<artifactId>commons-compress</artifactId>
+			<version>1.26.2</version>
 		</dependency>
 	</dependencies>
 	<dependencyManagement>

+ 3 - 3
src/main/java/cn/jlsxwkj/common/R/ResponseError.java

@@ -4,8 +4,8 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 /**
- *  @author zh
- *  异常封装类
+ * @author zh
+ * 异常封装类
  */
 @EqualsAndHashCode(callSuper = true)
 @Data
@@ -16,7 +16,7 @@ public class ResponseError extends Response {
      */
     private String exception;
 
-    public static ResponseError data(String exception){
+    public static ResponseError data(String exception) {
         ResponseError r = new ResponseError();
         r.setCode(500);
         r.setMessage("服务器异常");

+ 1 - 0
src/main/java/cn/jlsxwkj/common/config/SaTokenConfigure.java

@@ -14,6 +14,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
 
     /**
      * 注册拦截器
+     *
      * @param registry 注册
      */
     @Override

+ 19 - 0
src/main/java/cn/jlsxwkj/common/config/UserConfig.java

@@ -0,0 +1,19 @@
+package cn.jlsxwkj.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author zh
+ */
+@ConfigurationProperties(prefix = UserConfig.USER_PREFIX)
+@Component
+@Data
+public class UserConfig {
+
+    public static final String USER_PREFIX = "customer";
+
+    private String cnocrUrl;
+    private String sysMessage;
+}

+ 11 - 0
src/main/java/cn/jlsxwkj/common/exception/AccountPasswordCheckFailException.java

@@ -0,0 +1,11 @@
+package cn.jlsxwkj.common.exception;
+
+/**
+ * @author zh
+ */
+public class AccountPasswordCheckFailException extends CustomException{
+
+    public AccountPasswordCheckFailException(String message) {
+        super(message);
+    }
+}

+ 4 - 1
src/main/java/cn/jlsxwkj/common/exception/InsertFailException.java

@@ -2,9 +2,12 @@ package cn.jlsxwkj.common.exception;
 
 /**
  * @author zh
- * 插入失败
  */
 public class InsertFailException extends CustomException {
+
+    /**
+     * 插入失败
+     */
     public InsertFailException(String message) {
         super(message);
     }

+ 1 - 1
src/main/java/cn/jlsxwkj/common/exception/LoginAccountAlreadyLoginException.java

@@ -3,7 +3,7 @@ package cn.jlsxwkj.common.exception;
 /**
  * @author zh
  */
-public class LoginAccountAlreadyLoginException extends CustomException{
+public class LoginAccountAlreadyLoginException extends CustomException {
 
     /**
      * 重复登录

+ 1 - 1
src/main/java/cn/jlsxwkj/common/exception/LoginWrongPasswordException.java

@@ -3,7 +3,7 @@ package cn.jlsxwkj.common.exception;
 /**
  * @author zh
  */
-public class LoginWrongPasswordException extends CustomException{
+public class LoginWrongPasswordException extends CustomException {
 
     /**
      * 登录密码错误

+ 4 - 3
src/main/java/cn/jlsxwkj/common/exception/UnknownException.java

@@ -5,9 +5,10 @@ package cn.jlsxwkj.common.exception;
  */
 public class UnknownException extends CustomException {
 
-    /**
-     *
-     */
+    public UnknownException() {
+        this("未知错误");
+    }
+
     public UnknownException(String message) {
         super(message);
     }

+ 1 - 1
src/main/java/cn/jlsxwkj/common/filter/ExecTime.java

@@ -21,7 +21,7 @@ public class ExecTime implements Filter {
                          FilterChain filterChain) throws IOException, ServletException {
         long startTime = System.currentTimeMillis();
         String url = ((HttpServletRequest) servletRequest).getRequestURL().toString();
-        filterChain.doFilter(servletRequest,servletResponse);
+        filterChain.doFilter(servletRequest, servletResponse);
         long execTime = System.currentTimeMillis() - startTime;
         if (execTime > EXEC_TIME) {
             Log.warn(this.getClass(), "api: {}, exec_time ====> {}ms", url, execTime);

+ 8 - 4
src/main/java/cn/jlsxwkj/common/handler/GlobalRestExceptionHandler.java

@@ -25,36 +25,40 @@ public class GlobalRestExceptionHandler {
 
     /**
      * 捕捉全局异常
+     *
      * @param e 异常
      * @return 封装异常对象
      */
     @ExceptionHandler(Throwable.class)
     public ResponseError exception(Exception e) {
+
         ResponseError responseError = ResponseError.data(e.getClass().getName());
+
         if (e instanceof CustomException) {
             responseError.setMessage(e.getMessage());
         }
         if (e instanceof ErrorResponse) {
-            responseError.setCode(((ErrorResponse)e).getStatusCode().value());
+            responseError.setCode(((ErrorResponse) e).getStatusCode().value());
             responseError.setMessage(e.getMessage());
         }
         if (e instanceof SaTokenException) {
-            responseError.setCode(((SaTokenException)e).getCode());
+            responseError.setCode(((SaTokenException) e).getCode());
             responseError.setMessage("账户未登录");
         }
+
         LogError errorHandlerToLogError = new LogError().castResponseErrorToLogError(responseError);
         errorHandlerToLogError.setErrorInfo(e.getMessage());
         errorHandlerToLogError.setErrorStackTrace(Arrays.toString(e.getStackTrace()));
-
         try {
             logErrorService.insertOne(errorHandlerToLogError);
         } catch (InsertFailException insertFailException) {
-            insertFailException.printStackTrace();
+            Log.warn(this.getClass(), insertFailException.getMessage());
         }
 
         Log.error(e.getClass(), "Code       ====> {}", responseError.getCode());
         Log.error(e.getClass(), "Exception  ====> {}", responseError.getException());
         Log.error(e.getClass(), "Message    ====> {}", responseError.getMessage());
+
         e.printStackTrace();
 
         return responseError;

+ 0 - 1
src/main/java/cn/jlsxwkj/common/handler/ResultResponseHandler.java

@@ -17,7 +17,6 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
 import java.util.Arrays;
 
 /**
- *
  * @Author Joker DJ
  * ControllerAdvice 绑定的Controller包的权限定名 例如:com.dj.Controller
  */

+ 16 - 57
src/main/java/cn/jlsxwkj/common/reader/ParagraphDocReader.java

@@ -9,10 +9,9 @@ import org.apache.tika.sax.BodyContentHandler;
 import org.springframework.ai.document.Document;
 import org.springframework.ai.document.DocumentReader;
 import org.springframework.ai.reader.ExtractedTextFormatter;
-import org.springframework.core.io.DefaultResourceLoader;
-import org.springframework.core.io.Resource;
 import org.xml.sax.ContentHandler;
 
+import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.util.List;
 
@@ -27,73 +26,33 @@ public class ParagraphDocReader implements DocumentReader {
     private final ContentHandler handler;
     private final Metadata metadata;
     private final ParseContext context;
-    private final Resource resource;
+    private final InputStream inputStream;
+    private final String fileName;
     private int windowSize;
     private ExtractedTextFormatter textFormatter;
 
-    /**
-     * Constructor initializing the reader with a given resource URL.
-     * @param resourceUrl URL to the resource
-     */
-    public ParagraphDocReader(String resourceUrl,
-                              int windowSize) {
-        this(resourceUrl, ExtractedTextFormatter.defaults());
-        this.windowSize = windowSize;
-    }
-
-    /**
-     * Constructor initializing the reader with a given resource URL and a text formatter.
-     * @param resourceUrl URL to the resource
-     * @param textFormatter Formatter for the extracted text
-     */
-    public ParagraphDocReader(String resourceUrl,
-                              ExtractedTextFormatter textFormatter) {
-        this(new DefaultResourceLoader().getResource(resourceUrl), textFormatter);
-    }
 
-    /**
-     * Constructor initializing the reader with a resource and a text formatter. This
-     * constructor will create a BodyContentHandler that allows for reading large PDFs
-     * (constrained only by memory)
-     * @param resource Resource pointing to the document
-     * @param textFormatter Formatter for the extracted text
-     */
-    public ParagraphDocReader(Resource resource,
-                              ExtractedTextFormatter textFormatter) {
-        this(resource, new BodyContentHandler(-1), textFormatter);
-    }
-
-    /**
-     * Constructor initializing the reader with a resource, content handler, and a text
-     * formatter.
-     * @param resource Resource pointing to the document
-     * @param contentHandler Handler to manage content extraction
-     * @param textFormatter Formatter for the extracted text
-     */
-    public ParagraphDocReader(Resource resource,
-                              ContentHandler contentHandler,
-                              ExtractedTextFormatter textFormatter) {
-        this.textFormatter = textFormatter;
+    public ParagraphDocReader(byte[] bytes,
+                              String fileName,
+                              int windowSize) {
+        this.textFormatter = ExtractedTextFormatter.defaults();
         this.parser = new AutoDetectParser();
-        this.handler = contentHandler;
+        this.handler = new BodyContentHandler(-1);
         this.metadata = new Metadata();
         this.context = new ParseContext();
-        this.resource = resource;
+        this.inputStream = new ByteArrayInputStream(bytes);
+        this.windowSize = windowSize;
+        this.fileName = fileName;
     }
 
-    /**
-     * Extracts and returns the list of documents from the resource.
-     * @return List of extracted {@link Document}
-     */
     @Override
     public List<Document> get() {
-        try (InputStream stream = this.resource.getInputStream()) {
-            this.parser.parse(stream, this.handler, this.metadata, this.context);
-            return SplitDocument.splitStringToListDocument(this.handler.toString(), this.windowSize, this.resource.getFilename());
-        }
-        catch (Exception e) {
-            throw new RuntimeException(e);
+        try {
+            this.parser.parse(this.inputStream, this.handler, this.metadata, this.context);
+        } catch (Exception e) {
+            e.printStackTrace();
         }
+        return SplitDocument.splitStringToListDocument(this.handler.toString(), this.windowSize, this.fileName);
     }
 
 }

+ 49 - 0
src/main/java/cn/jlsxwkj/common/reader/ParagraphOcrReader.java

@@ -0,0 +1,49 @@
+package cn.jlsxwkj.common.reader;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.jlsxwkj.common.utils.SplitDocument;
+import lombok.Data;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.document.DocumentReader;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * @author zh
+ */
+@Data
+public class ParagraphOcrReader implements DocumentReader {
+
+    private final String cnocrUrl;
+    private final byte[] bytes;
+    private final Charset charset = StandardCharsets.UTF_8;
+    private final int windowSize;
+    private final String fileName;
+
+    public ParagraphOcrReader(byte[] bytes,
+                              String fileName,
+                              String cnocrUrl,
+                              int windowSize) {
+        this.bytes = bytes;
+        this.windowSize = windowSize;
+        this.cnocrUrl = cnocrUrl;
+        this.fileName = fileName;
+    }
+
+    @Override
+    public List<Document> get() {
+        String image = HttpRequest.post(cnocrUrl)
+                .form("image", bytes, fileName)
+                .execute()
+                .body();
+        StringBuilder sb = new StringBuilder();
+        new JSONArray(new JSONObject(image).get("results"))
+                .forEach(x -> sb.append(new JSONObject(x).get("text")).append(" "));
+        String document = sb.toString();
+        return SplitDocument.splitStringToListDocument(document, this.windowSize, this.fileName);
+    }
+}

+ 13 - 19
src/main/java/cn/jlsxwkj/common/reader/ParagraphTextReader.java

@@ -5,15 +5,14 @@ import cn.jlsxwkj.common.utils.SplitDocument;
 import lombok.Data;
 import org.springframework.ai.document.Document;
 import org.springframework.ai.document.DocumentReader;
-import org.springframework.core.io.DefaultResourceLoader;
-import org.springframework.core.io.Resource;
 import org.springframework.util.StreamUtils;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
-import java.util.Objects;
 
 /**
  * @author zh
@@ -22,23 +21,17 @@ import java.util.Objects;
 @Data
 public class ParagraphTextReader implements DocumentReader {
 
-    private final Resource resource;
+    private final InputStream inputStream;
     private final Charset charset = StandardCharsets.UTF_8;
-
-    /**
-     * 窗口大小,为段落的数量,用于滚动读取
-     */
     private final int windowSize;
+    private final String fileName;
 
-    public ParagraphTextReader(String resourceUrl,
+    public ParagraphTextReader(byte[] bytes,
+                               String fileName,
                                int windowSize) {
-        this(new DefaultResourceLoader().getResource(resourceUrl), windowSize);
-    }
-
-    public ParagraphTextReader(Resource resource, int windowSize) {
-        Objects.requireNonNull(resource, "The Spring Resource must not be null");
-        this.resource = resource;
+        this.inputStream = new ByteArrayInputStream(bytes);
         this.windowSize = windowSize;
+        this.fileName = fileName;
     }
 
     /**
@@ -48,12 +41,13 @@ public class ParagraphTextReader implements DocumentReader {
      */
     @Override
     public List<Document> get() {
+        String document = null;
         try {
-            String document = StreamUtils.copyToString(this.resource.getInputStream(), this.charset);
-            return SplitDocument.splitStringToListDocument(document, this.windowSize, this.resource.getFilename());
+            document = StreamUtils.copyToString(inputStream, this.charset);
         } catch (IOException e) {
-            throw new RuntimeException(e);
+            e.printStackTrace();
         }
+        assert document != null;
+        return SplitDocument.splitStringToListDocument(document, this.windowSize, this.fileName);
     }
-
 }

+ 37 - 0
src/main/java/cn/jlsxwkj/common/utils/CheckPassword.java

@@ -0,0 +1,37 @@
+package cn.jlsxwkj.common.utils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author LL
+ */
+public class CheckPassword {
+    /**
+     * 密码长度为8到20位,必须包含字母和数字,字母区分大小写
+     */
+    private static final String REG_EX1 = "^(?=.*[0-9])(?=.*[a-zA-Z])(.{8,20})$";
+    /**
+     * 密码中必须包含字母、数字、特称字符,至少8个字符,最多16个字符
+     */
+    private static final String REG_EX2 = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9])(.{8,16})$";
+
+    /**
+     * 密码长度为8到20位,必须包含字母和数字,字母区分大小写
+     */
+    public static boolean checkPassword(String password) {
+        Pattern passwordPattern = Pattern.compile(REG_EX1);
+        Matcher matcher = passwordPattern.matcher(password);
+        return matcher.matches();
+    }
+
+    /**
+     * 密码中必须包含字母、数字、特称字符,至少8个字符,最多16个字符
+     */
+    @SuppressWarnings("unused")
+    public static boolean password(String password) {
+        Pattern passwordPattern = Pattern.compile(REG_EX2);
+        Matcher matcher = passwordPattern.matcher(password);
+        return matcher.matches();
+    }
+}

+ 4 - 12
src/main/java/cn/jlsxwkj/common/utils/MergeDocuments.java

@@ -4,7 +4,6 @@ import cn.hutool.core.util.ArrayUtil;
 import org.springframework.ai.document.Document;
 
 import java.util.*;
-import java.util.stream.Collector;
 import java.util.stream.Collectors;
 
 /**
@@ -15,22 +14,15 @@ public class MergeDocuments {
     /**
      * 合并文档列表
      *
-     * @param documentList  文档列表
-     * @return              合并后的文档列表
+     * @param documentList 文档列表
+     * @return 合并后的文档列表
      */
     public static List<Document> mergeDocuments(List<Document> documentList) {
         ArrayList<Document> mergeDocuments = new ArrayList<>();
         //根据文档来源进行分组
-        Collector<Document, ?, Map<String, List<Document>>> groupDoc = Collectors.groupingBy(
-                (Document document) -> (String) document.getMetadata().get(SplitDocument.FILE_NAME)
-        );
-        Map<String, List<Document>> documentMap = documentList.stream().collect(groupDoc);
-        for (Map.Entry<String, List<Document>> docListEntry : documentMap.entrySet()) {
+        for (var docListEntry : documentList.stream().collect(Collectors.groupingBy(document -> (String) document.getMetadata().get(SplitDocument.FILE_NAME))).entrySet()) {
             //获取最大的段落结束编码
-            Comparator<Document> endPage = Comparator.comparing(
-                    (Document document) -> (int) document.getMetadata().get(SplitDocument.END_PARAGRAPH_NUMBER)
-            );
-            Optional<Document> documents = docListEntry.getValue().stream().max(endPage);
+            Optional<Document> documents = docListEntry.getValue().stream().max(Comparator.comparing(document -> (int) document.getMetadata().get(SplitDocument.END_PARAGRAPH_NUMBER)));
             if (documents.isEmpty()) {
                 continue;
             }

+ 7 - 6
src/main/java/cn/jlsxwkj/common/utils/SplitDocument.java

@@ -20,13 +20,14 @@ public class SplitDocument {
     /**
      * 分割字符并转换为多个文档
      *
-     * @param document      文档
-     * @param windowSize    窗口大小
-     * @param fileName      文件名
-     * @return              多个文档
+     * @param document   文档
+     * @param windowSize 窗口大小
+     * @param fileName   文件名
+     * @return 多个文档
      */
     public static List<Document> splitStringToListDocument(String document,
-                                                           int windowSize, String fileName) {
+                                                           int windowSize,
+                                                           String fileName) {
         ArrayList<Document> readDocuments = new ArrayList<>();
         List<String> paragraphs = Arrays.stream(document.split("\n")).distinct().dropWhile(""::equals).collect(Collectors.toList());
         //采用窗口滑动读取
@@ -48,7 +49,7 @@ public class SplitDocument {
      * @param paragraphList     段落内容列表
      * @param startParagraphNum 开始段落编码
      * @param endParagraphNum   结束段落编码
-     * @return                  文档信息
+     * @return 文档信息
      */
     private static Document toDocument(List<String> paragraphList,
                                        String fileName,

+ 34 - 29
src/main/java/cn/jlsxwkj/moudles/chat/ChatController.java

@@ -10,8 +10,6 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 import reactor.core.publisher.Flux;
 
-import java.io.IOException;
-
 /**
  * @author zh
  */
@@ -20,31 +18,38 @@ import java.io.IOException;
 @Tag(name = "RAG chat")
 public class ChatController {
 
-	@Resource
-	private ChatService chatService;
-
-	@Operation(summary = "上传文档")
-	@PostMapping("/upload")
-	@SaIgnore
-	public String uploadDoc(@RequestBody MultipartFile file) throws IOException, CustomException {
-		return chatService.uploadDocument(file);
-	}
-
-	@Operation(summary = "搜索文档")
-	@PostMapping("/search")
-	public String searchDoc(@RequestParam String keyword) {
-		return chatService.search(keyword);
-	}
-
-	@Operation(summary = "问答文档流")
-	@PostMapping(value = "/chatStream", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
-	public Flux<String> chatStream(@RequestParam String message) {
-		return chatService.chatStream(message);
-	}
-
-	@Operation(summary = "停止回答")
-	@GetMapping(value = "/stopChat")
-	public void stopChat() {
-		chatService.stopChat();
-	}
+    @Resource
+    private ChatService chatService;
+
+    @Operation(summary = "上传文档")
+    @PostMapping("/upload")
+    @SaIgnore
+    public String uploadDoc(@RequestBody MultipartFile file) throws CustomException {
+        return chatService.uploadDocument(file);
+    }
+
+    @Operation(summary = "搜索文档")
+    @PostMapping("/search")
+    public String searchDoc(@RequestParam String keyword) {
+        return chatService.search(keyword);
+    }
+
+    @Operation(summary = "问答文档流")
+    @PostMapping(value = "/chatStream", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
+    public Flux<String> chatStream(@RequestParam String message) {
+        return chatService.chatStream(message);
+    }
+
+    @Operation(summary = "问答文档总结流")
+    @PostMapping(value = "/chatSummaryStream", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
+    public Flux<String> chatSumStream(@RequestParam String message,
+                                      @RequestBody MultipartFile file) throws CustomException {
+        return chatService.chatSummaryStream(message, file);
+    }
+
+    @Operation(summary = "停止回答")
+    @GetMapping(value = "/stopChat")
+    public void stopChat() {
+        chatService.stopChat();
+    }
 }

+ 194 - 150
src/main/java/cn/jlsxwkj/moudles/chat/ChatService.java

@@ -1,9 +1,15 @@
 package cn.jlsxwkj.moudles.chat;
 
 import cn.dev33.satoken.SaManager;
+import cn.hutool.core.lang.Tuple;
 import cn.hutool.crypto.digest.MD5;
-import cn.jlsxwkj.common.exception.*;
+import cn.jlsxwkj.common.config.UserConfig;
+import cn.jlsxwkj.common.exception.CustomException;
+import cn.jlsxwkj.common.exception.FileTypeDoesNotSupportException;
+import cn.jlsxwkj.common.exception.InsertFailException;
+import cn.jlsxwkj.common.exception.UnknownException;
 import cn.jlsxwkj.common.reader.ParagraphDocReader;
+import cn.jlsxwkj.common.reader.ParagraphOcrReader;
 import cn.jlsxwkj.common.reader.ParagraphTextReader;
 import cn.jlsxwkj.common.utils.Log;
 import cn.jlsxwkj.common.utils.MergeDocuments;
@@ -11,10 +17,7 @@ import cn.jlsxwkj.moudles.chathistory.ChatHistoryService;
 import jakarta.annotation.Resource;
 import lombok.Data;
 import org.reactivestreams.Subscription;
-import org.springframework.ai.chat.messages.AssistantMessage;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.messages.SystemMessage;
-import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.messages.*;
 import org.springframework.ai.chat.model.ChatResponse;
 import org.springframework.ai.chat.prompt.Prompt;
 import org.springframework.ai.document.Document;
@@ -42,149 +45,190 @@ import java.util.stream.Collectors;
 @Data
 public class ChatService {
 
-	/**
-	 * md5 校验
-	 */
-	private static final MD5 MD5 = cn.hutool.crypto.digest.MD5.create();
-	/**
-	 * 当前项目路径 + /ai_doc
-	 */
-	private static final String PATH = Objects.requireNonNull(ChatService.class.getClassLoader().getResource("")).getPath() + "\\ai_doc\\";
-	/**
-	 * 用户对话上下文
-	 */
-	private static final List<Message> LIST_MESSAGE = new ArrayList<>();
-	/**
-	 * 向量
-	 */
-	@Resource
-	private VectorStore vectorStore;
-	/**
-	 * 大模型
-	 */
-	@Resource
-	private OllamaChatModel ollamaChatModel;
-	/**
-	 * 历史记录
-	 */
-	@Resource
-	private ChatHistoryService chatHistoryService;
-	/**
-	 * 单条消息
-	 */
-	private static StringBuffer chatMessage;
-	/**
-	 * 上游订阅
-	 */
-	private Subscription subscription;
-
-	/**
-	 * 使用spring ai解析txt文档并保存至 pg
-	 * @param file 			保存的文件
-	 * @return 				保存状态
-	 * @throws IOException 	保存失败
-	 */
-	public String uploadDocument(MultipartFile file) throws CustomException, IOException {
-		//分割文件名为 文件名, 后缀
-		String[] split = Objects.requireNonNull(file.getOriginalFilename()).split("\\.");
-		//获取文件类型
-		String fileType = split[split.length - 1].toLowerCase(Locale.ROOT);
-		//文件夹地址
-		File savePath = new File(PATH);
-		//保存文件对象
-		File saveFile = new File(PATH + ChatService.MD5.digestHex(file.getInputStream()) + "." + fileType);
-		//文件保存地址
-		String fileUrl = saveFile.toURI().toURL().toString();
-		//判断文件夹是否存在
-		if (!savePath.exists()) {
-			if (!savePath.mkdirs()) {
-				Log.warn(this.getClass(), "创建文件夹失败: " + PATH);
-			}
-		}
-		//判断文件类型
-		DocumentReader reader;
-		switch (fileType) {
-			case "txt" -> reader = new ParagraphTextReader(fileUrl, 5);
-			case "doc", "docx" -> reader = new ParagraphDocReader(fileUrl, 5);
-			default -> throw new FileTypeDoesNotSupportException("暂不支持的文件类型: " + fileType);
-		}
-		// 判断文件是否存在
-		if (!saveFile.exists()) {
-			try	{
-				file.transferTo(saveFile);
-				vectorStore.add(reader.get());
-				return "上传完成";
-			} catch (Exception e){
-				boolean delete = saveFile.delete();
-				if (!delete) {
-					throw new FileDeleteFailException("删除文件失败: " + fileUrl);
-				}
-			}
-		}
-		throw new UnknownException("未知错误");
-	}
-
-	/**
-	 * 根据关键词搜索向量库
-	 *
-	 * @param keyword 	关键词
-	 * @return 			文本内容
-	 */
-	public String search(String keyword) {
-		return MergeDocuments.mergeDocuments(
-				vectorStore.similaritySearch(SearchRequest.query(keyword).withSimilarityThreshold(0.5))
-		).stream().map(Document::getContent).collect(Collectors.joining("\n"));
-	}
-
-	/**
-	 * 问答流,根据输入内容回答
-	 *
-	 * @param message 	输入内容
-	 * @return 			回答内容
-	 */
-	public Flux<String> chatStream(String message) {
-		String userId = SaManager.getStpLogic("").getLoginIdAsString();
-		chatMessage = new StringBuffer();
-		//查询获取文档信息
-		String content = search(message);
-		//封装prompt并调用大模型
-		Flux<ChatResponse> stream = this.ollamaChatModel.stream(getChatPrompt2String(message, content));
-		return stream.doOnSubscribe(subscription -> this.subscription = subscription)
-				.map(response -> {
-					String str = response.getResult().getOutput().getContent();
-					chatMessage.append(str);
-					return str.replace(" ", "  ");
-				}).concatWithValues("<{完成}>")
-				.onErrorComplete()
-				.doOnComplete(() -> {
-					LIST_MESSAGE.add(new AssistantMessage(chatMessage.toString()));
-					try {
-						chatHistoryService.insert(userId, message, chatMessage.toString());
-					} catch (InsertFailException e) {
-						e.printStackTrace();
-					}
-				});
-	}
-
-	/**
-	 * 取消订阅
-	 */
-	public void stopChat() {
-		subscription.cancel();
-	}
-
-	/**
-	 * 获取prompt
-	 *
-	 * @param message 	提问内容
-	 * @param context 	上下文
-	 * @return 			提示词
-	 */
-	private Prompt getChatPrompt2String(String message, String context) {
-		Log.info(this.getClass(),"{} message : {}", "user", message);
-		LIST_MESSAGE.add(new SystemMessage("请永远说你是玄武科技公司是 ai 或者 大模型"));
-		LIST_MESSAGE.add(new SystemMessage(context));
-		LIST_MESSAGE.add(new UserMessage(message));
-		return new Prompt(LIST_MESSAGE);
-	}
+    private static final MD5 MD5 = cn.hutool.crypto.digest.MD5.create();
+    private static final String PATH = Objects.requireNonNull(ChatService.class.getClassLoader().getResource("")).getPath() + "\\ai_doc\\";
+    private static final List<Message> LIST_MESSAGE = new ArrayList<>();
+
+    @Resource
+    private VectorStore vectorStore;
+    @Resource
+    private OllamaChatModel ollamaChatModel;
+    @Resource
+    private ChatHistoryService chatHistoryService;
+    @Resource
+    private UserConfig userConfig;
+
+    private StringBuffer chatMessage;
+    private Subscription subscription;
+
+    /**
+     * 使用spring ai解析txt文档并保存至 pg
+     *
+     * @param file 保存的文件
+     * @return 保存状态
+     */
+    public String uploadDocument(MultipartFile file) throws CustomException {
+        Tuple supportFile = isSupportFile(file);
+        File saveFile = supportFile.get(0);
+        File fileName = supportFile.get(1);
+        DocumentReader reader = supportFile.get(3);
+        if (!saveFile.exists()) {
+            try {
+                file.transferTo(saveFile);
+                vectorStore.add(reader.get());
+                return "上传完成";
+            } catch (Exception e) {
+                boolean delete = saveFile.delete();
+                if (!delete) {
+                    Log.warn(this.getClass(), "删除文件失败: {}", fileName);
+                    throw new UnknownException();
+                }
+            }
+        }
+        return "上传完成";
+    }
+
+    /**
+     * 根据关键词搜索向量库
+     *
+     * @param keyword 关键词
+     * @return 文本内容
+     */
+    public String search(String keyword) {
+        return mergeDocuments(vectorStore.similaritySearch(SearchRequest.query(keyword).withSimilarityThreshold(0.5)));
+    }
+
+    /**
+     * 问答流,根据输入内容回答
+     *
+     * @param message 输入内容
+     * @return 回答内容
+     */
+    public Flux<String> chatStream(String message) {
+        String context = search(message);
+        return stream(message, new SystemMessage(context));
+    }
+
+    /**
+     * 问答总结流, 根据给定的文档总结内容
+     *
+     * @param message 输入内容
+     * @param file    文件
+     * @return 内容总结
+     */
+    public Flux<String> chatSummaryStream(String message, MultipartFile file) throws CustomException {
+        Tuple supportFile = isSupportFile(file);
+        String fileName = supportFile.get(1);
+        String fileTypeCn = supportFile.get(2);
+        DocumentReader reader = supportFile.get(3);
+        String context = fileTypeCn + " : " + fileName + " ====> " + mergeDocuments(reader.get()).replace("\n", " ");
+        Log.info(this.getClass(), context);
+        return stream(message, new UserMessage(context + " : " + message));
+    }
+
+    /**
+     * 取消订阅
+     */
+    public void stopChat() {
+        subscription.cancel();
+    }
+
+    private String mergeDocuments(List<Document> documents) {
+        return MergeDocuments.mergeDocuments(documents)
+                .stream().map(Document::getContent).collect(Collectors.joining("\n"));
+    }
+
+    /**
+     * 获取prompt
+     *
+     * @param message 提问内容
+     * @param context 提示词
+     * @return 提示词
+     */
+    private Prompt getChatPrompt2String(String message, Message context) {
+        Log.info(this.getClass(), "{} message : {}", "user", message);
+        ChatService.LIST_MESSAGE.add(new SystemMessage(userConfig.getSysMessage()));
+        if (context.getMessageType().equals(MessageType.SYSTEM)) {
+            ChatService.LIST_MESSAGE.add(new UserMessage(message));
+            ChatService.LIST_MESSAGE.add(context);
+        }
+        if (context.getMessageType().equals(MessageType.USER)) {
+            ChatService.LIST_MESSAGE.add(context);
+        }
+        return new Prompt(ChatService.LIST_MESSAGE);
+    }
+
+    /**
+     * chat stream 封装
+     *
+     * @param message 提问内容
+     * @param context 提示词
+     * @return flux
+     */
+    private Flux<String> stream(String message, Message context) {
+        String userId = SaManager.getStpLogic("").getLoginIdAsString();
+        chatMessage = new StringBuffer();
+        Flux<ChatResponse> stream = ollamaChatModel.stream(getChatPrompt2String(message, context));
+        return stream.doOnSubscribe(subscription -> this.subscription = subscription).map(
+                response -> {
+                    String str = response.getResult().getOutput().getContent();
+                    chatMessage.append(str);
+                    return str.replace(" ", "  ");
+                }).concatWithValues("<{完成}>")
+                .onErrorComplete()
+                .doOnComplete(() -> {
+                    String chatMsg = chatMessage.toString();
+                    ChatService.LIST_MESSAGE.add(new AssistantMessage(chatMsg));
+                    try {
+                        chatHistoryService.insert(userId, message, chatMsg);
+                    } catch (InsertFailException e) {
+                        Log.warn(this.getClass(), e.getMessage());
+                    }
+                    Log.info(this.getClass(), "{} user message ==> {}", userId, message);
+                    Log.info(this.getClass(), "{} chat message ==> {}", userId, chatMsg);
+                });
+    }
+
+    /**
+     * 封装文件
+     *
+     * @param file 文件流
+     * @return 元组
+     */
+    private Tuple isSupportFile(MultipartFile file) throws CustomException {
+        byte[] bytes = new byte[0];
+        try {
+            bytes = file.getBytes();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        String[] split = Objects.requireNonNull(file.getOriginalFilename()).split("\\.");
+        String fileType = split[split.length - 1].toLowerCase(Locale.ROOT);
+        File savePath = new File(PATH);
+        File saveFile = new File(PATH + ChatService.MD5.digestHex(bytes) + "." + fileType);
+        String fileName = saveFile.getName();
+        String fileTypeCn;
+        if (!savePath.exists()) {
+            if (!savePath.mkdirs()) {
+                Log.warn(this.getClass(), "创建文件夹失败: " + PATH);
+            }
+        }
+        DocumentReader reader;
+        switch (fileType) {
+            case "txt" -> {
+                reader = new ParagraphTextReader(bytes, fileName, 5);
+                fileTypeCn = "文本";
+            }
+            case "doc", "docx" -> {
+                reader = new ParagraphDocReader(bytes, fileName, 5);
+                fileTypeCn = "文档";
+            }
+            case "jpg", "png" -> {
+                reader = new ParagraphOcrReader(bytes, fileName, userConfig.getCnocrUrl(), 5);
+                fileTypeCn = "图片";
+            }
+            default -> throw new FileTypeDoesNotSupportException("暂不支持的文件类型: " + fileType);
+        }
+        return new Tuple(saveFile, fileName, fileTypeCn, reader);
+    }
 }

+ 3 - 3
src/main/java/cn/jlsxwkj/moudles/chathistory/ChatHistory.java

@@ -11,8 +11,8 @@ import lombok.EqualsAndHashCode;
 @Data
 public class ChatHistory extends Dao {
 
-  private String userId;
-  private String userQ;
-  private String chatA;
+    private String userId;
+    private String userQ;
+    private String chatA;
 
 }

+ 12 - 12
src/main/java/cn/jlsxwkj/moudles/chathistory/ChatHistoryMapper.java

@@ -14,20 +14,20 @@ import java.util.List;
 public interface ChatHistoryMapper {
 
     @Insert("""
-insert into chat_history(user_id, "user_Q", "chat_A") 
-values (#{userId}, #{userQ}, #{chatA})
-""")
+            insert into chat_history(user_id, "user_Q", "chat_A") 
+            values (#{userId}, #{userQ}, #{chatA})
+            """)
     Integer insert(@Param("userId") String userId,
-                @Param("userQ") String userQ,
-                @Param("chatA") String chatA);
+                   @Param("userQ") String userQ,
+                   @Param("chatA") String chatA);
 
     @Select("""
-select *
-from chat_history
-where user_id = #{userId}
-and is_deleted = 0
-order by create_time desc
-limit 20
-""")
+            select *
+            from chat_history
+            where user_id = #{userId}
+            and is_deleted = 0
+            order by create_time desc
+            limit 20
+            """)
     List<ChatHistory> selectHistoryByUserId(@Param("userId") String userId);
 }

+ 2 - 4
src/main/java/cn/jlsxwkj/moudles/chathistory/ChatHistoryService.java

@@ -2,7 +2,6 @@ package cn.jlsxwkj.moudles.chathistory;
 
 import cn.dev33.satoken.SaManager;
 import cn.jlsxwkj.common.exception.InsertFailException;
-import cn.jlsxwkj.common.utils.Log;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -21,10 +20,9 @@ public class ChatHistoryService {
     @Transactional(rollbackFor = InsertFailException.class)
     public void insert(String userId, String userQ, String chatA) throws InsertFailException {
         Integer rows = chatHistoryMapper.insert(userId, userQ, chatA);
-        if (rows > 0) {
-            Log.info(this.getClass(), "插入成功");
+        if (rows < 1) {
+            throw new InsertFailException("插入失败");
         }
-        throw new InsertFailException("插入失败");
     }
 
     public List<ChatHistory> selectHistoryByUserId() {

+ 4 - 3
src/main/java/cn/jlsxwkj/moudles/logerror/LogErrorMapper.java

@@ -12,11 +12,12 @@ public interface LogErrorMapper {
 
     /**
      * 插入错误日志
+     *
      * @param logError log实体
      */
     @Insert("""
-insert into log_error(message, exception, error_info,error_stack_trace) 
-values (#{LogError.message}, #{LogError.exception},#{LogError.errorInfo},#{LogError.errorStackTrace})
-""")
+            insert into log_error(message, exception, error_info,error_stack_trace) 
+            values (#{LogError.message}, #{LogError.exception},#{LogError.errorInfo},#{LogError.errorStackTrace})
+            """)
     Integer insertOne(@Param("LogError") LogError logError);
 }

+ 2 - 4
src/main/java/cn/jlsxwkj/moudles/logerror/LogErrorService.java

@@ -1,7 +1,6 @@
 package cn.jlsxwkj.moudles.logerror;
 
 import cn.jlsxwkj.common.exception.InsertFailException;
-import cn.jlsxwkj.common.utils.Log;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -18,9 +17,8 @@ public class LogErrorService {
     @Transactional(rollbackFor = InsertFailException.class)
     public void insertOne(LogError logError) throws InsertFailException {
         Integer rows = logErrorMapper.insertOne(logError);
-        if (rows > 0) {
-            Log.info(this.getClass(), "插入错误完成");
+        if (rows < 1) {
+            throw new InsertFailException("插入失败");
         }
-        throw new InsertFailException("插入失败");
     }
 }

+ 1 - 2
src/main/java/cn/jlsxwkj/moudles/userlist/UserListController.java

@@ -20,8 +20,7 @@ public class UserListController {
     @Operation(summary = "登录")
     @PostMapping("/login")
     @SaIgnore
-    public UserVO login(@RequestParam String userName,
-                        @RequestParam String userPassword) throws CustomException {
+    public UserVO login(@RequestParam String userName, @RequestParam String userPassword) throws CustomException {
         return userListService.checkOrAddUser(userName, userPassword);
     }
 

+ 9 - 9
src/main/java/cn/jlsxwkj/moudles/userlist/UserListMapper.java

@@ -12,17 +12,17 @@ import org.apache.ibatis.annotations.Select;
 public interface UserListMapper {
 
     @Insert("""
-insert into user_list(user_id, user_name, user_password) 
-values (#{user.userId}, #{user.userName}, #{user.userPassword})
-""")
+            insert into user_list(user_id, user_name, user_password) 
+            values (#{user.userId}, #{user.userName}, #{user.userPassword})
+            """)
     Integer authUser(@Param("user") UserList user);
 
     @Select("""
-select * 
-from user_list
-where user_name = #{userName} 
-and is_deleted = 0
-limit 1
-""")
+            select * 
+            from user_list
+            where user_name = #{userName} 
+            and is_deleted = 0
+            limit 1
+            """)
     UserList checkUser(@Param("userName") String userName);
 }

+ 5 - 4
src/main/java/cn/jlsxwkj/moudles/userlist/UserListService.java

@@ -3,10 +3,8 @@ package cn.jlsxwkj.moudles.userlist;
 import cn.dev33.satoken.secure.SaSecureUtil;
 import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.lang.Snowflake;
-import cn.jlsxwkj.common.exception.AccountAuthFailException;
-import cn.jlsxwkj.common.exception.CustomException;
-import cn.jlsxwkj.common.exception.LoginAccountAlreadyLoginException;
-import cn.jlsxwkj.common.exception.LoginWrongPasswordException;
+import cn.jlsxwkj.common.exception.*;
+import cn.jlsxwkj.common.utils.CheckPassword;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -23,6 +21,9 @@ public class UserListService {
 
     @Transactional(rollbackFor = AccountAuthFailException.class)
     public UserVO checkOrAddUser(String userName, String userPassword) throws CustomException {
+        if (!CheckPassword.checkPassword(userPassword)) {
+            throw new AccountPasswordCheckFailException("密码长度为8到20位,必须包含字母和数字,字母区分大小写");
+        }
         String sha512pwd = SaSecureUtil.sha512(userPassword);
         UserList userList = userListMapper.checkUser(userName);
         UserVO userVO = new UserVO();

+ 4 - 1
src/main/resources/application-dev.yml

@@ -11,6 +11,9 @@ spring:
     ollama:
       base-url: http://localhost:11434
       chat:
-        model: qwen2:7b
+        model: qwen2:1.5b
       embedding:
         model: mofanke/dmeta-embedding-zh
+customer:
+  cnocr-url: "http://www.jlsxwkj.cn:8501/ocr"
+  sysMessage: "如果问你是谁, 请说你是玄武科技公司是 ai 或者 大模型"

+ 1 - 2
src/main/resources/application.yml

@@ -15,7 +15,7 @@ spring:
 # spring doc-openapi项目配置`
 springdoc:
   swagger-ui:
-    path: /api-cn.jlsxwkj.common.test
+    path: /api
     tags-sorter: alpha
     operations-sorter: alpha
   api-docs:
@@ -37,7 +37,6 @@ logging:
 mybatis:
   configuration:
     map-underscore-to-camel-case: true
-    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 # sa token 鉴权
 sa-token:
   # token 名称(同时也是 cookie 名称)