first commit
This commit is contained in:
39
qingyun-service/pom.xml
Normal file
39
qingyun-service/pom.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>qingyun-plus</artifactId>
|
||||
<groupId>com.qingyun</groupId>
|
||||
<version>4.7.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>qingyun-service</artifactId>
|
||||
<description>业务模块</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.qingyun</groupId>
|
||||
<artifactId>qingyun-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.qingyun</groupId>
|
||||
<artifactId>qingyun-data-permission</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.qingyun</groupId>
|
||||
<artifactId>qingyun-mybatisplus</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.qingyun</groupId>
|
||||
<artifactId>qingyun-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.qingyun.service.agent;
|
||||
|
||||
import com.agentsflex.core.llm.Llm;
|
||||
import com.agentsflex.core.llm.response.AiMessageResponse;
|
||||
import com.agentsflex.core.message.SystemMessage;
|
||||
import com.agentsflex.core.prompt.TextPrompt;
|
||||
import com.agentsflex.llm.deepseek.DeepseekConfig;
|
||||
import com.agentsflex.llm.deepseek.DeepseekLlm;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class DeepseekLlmStrategy implements LlmStrategy {
|
||||
public final Llm llm;
|
||||
|
||||
public DeepseekLlmStrategy(DeepseekConfig config) {
|
||||
this.llm = new DeepseekLlm(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiMessageResponse chat(String promptTemplate, Map<String, String> variables) {
|
||||
String resolvedPrompt = resolveTemplate(promptTemplate, variables);
|
||||
TextPrompt prompt = new TextPrompt();
|
||||
prompt.setContent(resolvedPrompt);
|
||||
prompt.setSystemMessage(new SystemMessage(promptTemplate));
|
||||
return llm.chat(prompt);
|
||||
}
|
||||
|
||||
private String resolveTemplate(String template, Map<String, String> variables) {
|
||||
for (Map.Entry<String, String> entry : variables.entrySet()) {
|
||||
template = template.replace("${" + entry.getKey() + "}", entry.getValue());
|
||||
}
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.qingyun.service.agent;
|
||||
|
||||
import com.agentsflex.core.llm.response.AiMessageResponse;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public interface LlmStrategy {
|
||||
Logger logger = Logger.getLogger(LlmStrategy.class.getName());
|
||||
|
||||
AiMessageResponse chat(String promptTemplate, Map<String, String> variables);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.qingyun.service.agent;
|
||||
|
||||
import com.agentsflex.llm.deepseek.DeepseekConfig;
|
||||
import com.agentsflex.llm.openai.OpenAILlmConfig;
|
||||
import com.qingyun.service.domain.ContractModelConfig;
|
||||
|
||||
public class LlmStrategyFactory {
|
||||
|
||||
public static LlmStrategy getStrategy(ContractModelConfig contractModelConfig) {
|
||||
if (contractModelConfig == null) {
|
||||
// 默认使用OpenAI
|
||||
return new OpenAILlmStrategy(defaultOpenAIConfig());
|
||||
}
|
||||
switch (contractModelConfig.getModelName().toLowerCase()) {
|
||||
case "openai":
|
||||
return new OpenAILlmStrategy(createOpenAIConfig(contractModelConfig));
|
||||
case "deepseek":
|
||||
return new DeepseekLlmStrategy(createDeepseekConfig(contractModelConfig));
|
||||
default:
|
||||
// 默认使用OpenAI
|
||||
return new OpenAILlmStrategy(createOpenAIConfig(contractModelConfig));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static OpenAILlmConfig createOpenAIConfig(ContractModelConfig contractModelConfig) {
|
||||
OpenAILlmConfig config = new OpenAILlmConfig();
|
||||
config.setEndpoint(contractModelConfig.getModelUrl());
|
||||
config.setModel(contractModelConfig.getModelVersion());
|
||||
config.setApiKey(contractModelConfig.getModelKey());
|
||||
return config;
|
||||
}
|
||||
|
||||
private static DeepseekConfig createDeepseekConfig(ContractModelConfig contractModelConfig) {
|
||||
DeepseekConfig config = new DeepseekConfig();
|
||||
config.setEndpoint(contractModelConfig.getModelUrl());
|
||||
config.setModel(contractModelConfig.getModelVersion());
|
||||
config.setApiKey(contractModelConfig.getModelKey());
|
||||
return config;
|
||||
}
|
||||
|
||||
private static OpenAILlmConfig defaultOpenAIConfig() {
|
||||
OpenAILlmConfig config = new OpenAILlmConfig();
|
||||
config.setEndpoint("http://10.77.149.156:11434");
|
||||
config.setModel("qwen3:30b-a3b");
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.qingyun.service.agent;
|
||||
|
||||
import com.agentsflex.core.llm.Llm;
|
||||
import com.agentsflex.core.llm.response.AiMessageResponse;
|
||||
import com.agentsflex.core.message.SystemMessage;
|
||||
import com.agentsflex.core.prompt.TextPrompt;
|
||||
import com.agentsflex.llm.openai.OpenAILlm;
|
||||
import com.agentsflex.llm.openai.OpenAILlmConfig;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class OpenAILlmStrategy implements LlmStrategy {
|
||||
|
||||
public final Llm llm;
|
||||
|
||||
public OpenAILlmStrategy(OpenAILlmConfig config) {
|
||||
this.llm = new OpenAILlm(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiMessageResponse chat(String promptTemplate, Map<String, String> variables) {
|
||||
String resolvedPrompt = resolveTemplate(promptTemplate, variables);
|
||||
logger.info("resolvedPrompt: {}" + resolvedPrompt);
|
||||
TextPrompt prompt = new TextPrompt();
|
||||
prompt.setSystemMessage(new SystemMessage(resolvedPrompt));
|
||||
prompt.setContent("[]");
|
||||
return llm.chat(prompt);
|
||||
}
|
||||
|
||||
private String resolveTemplate(String template, Map<String, String> variables) {
|
||||
for (Map.Entry<String, String> entry : variables.entrySet()) {
|
||||
template = template.replace("${" + entry.getKey() + "}", entry.getValue());
|
||||
}
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.qingyun.service.agent;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Classname ResourceReadUtils
|
||||
* @Author dyh
|
||||
* @Date 2024/10/13 16:45
|
||||
*/
|
||||
public class ResourceReadUtils {
|
||||
|
||||
public static String convert(BufferedReader reader) throws IOException {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
stringBuilder.append(line);
|
||||
stringBuilder.append(System.lineSeparator()); // 保留换行符
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.qingyun.service.agent;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class TemplateManager {
|
||||
private static final Map<String, String> TEMPLATE_CACHE = new HashMap<>();
|
||||
|
||||
public static String getTemplate(String templateKey) {
|
||||
return TEMPLATE_CACHE.computeIfAbsent(templateKey, k -> {
|
||||
BufferedReader reader = ResourceUtil.getReader(k, Charset.defaultCharset());
|
||||
try {
|
||||
return ResourceReadUtils.convert(reader);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("加载模板文件失败: " + k, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class BoundingBox {
|
||||
float x;
|
||||
float y;
|
||||
float width;
|
||||
float height;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
|
||||
import com.github.difflib.patch.DeltaType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 变化位置信息类
|
||||
*/
|
||||
public class ChangeLocation {
|
||||
|
||||
public List<String> sourceText;
|
||||
public List<String> targetText;
|
||||
public DeltaType changeType;
|
||||
public List<TextElement> sourceElements;
|
||||
public List<TextElement> targetElements;
|
||||
|
||||
ChangeLocation(List<String> sourceText,
|
||||
List<String> targetText,
|
||||
DeltaType changeType,
|
||||
List<TextElement> sourceElements,
|
||||
List<TextElement> targetElements) {
|
||||
this.sourceText = sourceText;
|
||||
this.targetText = targetText;
|
||||
this.changeType = changeType;
|
||||
this.sourceElements = Objects.requireNonNull(sourceElements);
|
||||
this.targetElements = Objects.requireNonNull(targetElements);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
/**
|
||||
* 坐标转换工具类
|
||||
*/
|
||||
public class CoordinateConverter {
|
||||
|
||||
private static final float ORIGINAL_DPI = 72f;
|
||||
|
||||
/**
|
||||
* 将基于72 DPI的坐标转换为指定DPI的坐标
|
||||
*
|
||||
* @param originalCoordinate 原始坐标(基于72 DPI)
|
||||
* @param targetDpi 目标DPI(如150)
|
||||
* @return 转换后的坐标
|
||||
*/
|
||||
public static float convertCoordinate(float originalCoordinate, float targetDpi) {
|
||||
return originalCoordinate * (targetDpi / ORIGINAL_DPI);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将基于72 DPI的坐标转换为150 DPI的坐标
|
||||
*
|
||||
* @param originalCoordinate 原始坐标(基于72 DPI)
|
||||
* @return 转换后的坐标(基于150 DPI)
|
||||
*/
|
||||
public static float convertCoordinateTo150Dpi(float originalCoordinate) {
|
||||
return convertCoordinate(originalCoordinate, 150f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量转换坐标点
|
||||
*
|
||||
* @param originalCoordinates 原始坐标数组
|
||||
* @param targetDpi 目标DPI
|
||||
* @return 转换后的坐标数组
|
||||
*/
|
||||
public static float[] convertCoordinates(float[] originalCoordinates, float targetDpi) {
|
||||
float[] convertedCoordinates = new float[originalCoordinates.length];
|
||||
for (int i = 0; i < originalCoordinates.length; i++) {
|
||||
convertedCoordinates[i] = convertCoordinate(originalCoordinates[i], targetDpi);
|
||||
}
|
||||
return convertedCoordinates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DiffRegion {
|
||||
String changeType;
|
||||
String sourceText;
|
||||
String targetText;
|
||||
Integer sourcePage; // 源文件页码
|
||||
Integer sourceLine; // 源文件行号
|
||||
Integer targetPage; // 目标文件页码
|
||||
Integer targetLine; // 目标文件行号
|
||||
BoundingBox sourceBox;
|
||||
BoundingBox targetBox;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 判断两个文档是否具有可比性(LCS)
|
||||
*/
|
||||
public class DocumentStructureComparator {
|
||||
|
||||
public static boolean isComparable(List<String> doc1Summaries, List<String> doc2Summaries) {
|
||||
int lcsLength = longestCommonSubsequence(doc1Summaries, doc2Summaries);
|
||||
int minLength = Math.min(doc1Summaries.size(), doc2Summaries.size());
|
||||
double similarity = (double) lcsLength / minLength;
|
||||
|
||||
System.out.println("LCS 长度:" + lcsLength);
|
||||
System.out.println("最小页数:" + minLength);
|
||||
System.out.println("结构相似度:" + similarity);
|
||||
|
||||
// 设置一个阈值,比如 0.6
|
||||
return similarity >= 0.6;
|
||||
}
|
||||
|
||||
private static int longestCommonSubsequence(List<String> list1, List<String> list2) {
|
||||
int m = list1.size();
|
||||
int n = list2.size();
|
||||
|
||||
// 使用一维数组优化空间
|
||||
int[] dp = new int[n + 1];
|
||||
|
||||
for (int i = 1; i <= m; i++) {
|
||||
int prev = 0;
|
||||
for (int j = 1; j <= n; j++) {
|
||||
int current = dp[j];
|
||||
if (list1.get(i - 1).equals(list2.get(j - 1))) {
|
||||
dp[j] = prev + 1;
|
||||
} else {
|
||||
dp[j] = Math.max(dp[j], dp[j - 1]);
|
||||
}
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutableTriple;
|
||||
import org.apache.commons.lang3.tuple.Triple;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
public class FetchAllPagesContent {
|
||||
|
||||
@Value("${ocr.url}")
|
||||
private String orcServerUrl;
|
||||
|
||||
// 日志
|
||||
private static final Logger log = LoggerFactory.getLogger(FetchAllPagesContent.class);
|
||||
|
||||
// 获取所有页面内容
|
||||
public List<String> extractPDFPagesContent(File pdfFile) throws Exception {
|
||||
List<Triple<String, Integer, Boolean>> base64Pages = splitPdfToBase64Pages(pdfFile);
|
||||
if (CollectionUtil.isEmpty(base64Pages)) {
|
||||
throw new Exception("无法获取页面内容");
|
||||
}
|
||||
Files.deleteIfExists(pdfFile.toPath()); // 清理原始 PDF
|
||||
|
||||
List<String> contentList = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < base64Pages.size(); i++) {
|
||||
int pageNum = i + 1;
|
||||
Triple<String, Integer, Boolean> base64Page = base64Pages.get(i);
|
||||
String base64 = base64Page.getLeft();
|
||||
Integer pageData = base64Page.getMiddle(); // 获取页数信息
|
||||
Boolean scannedPdf = base64Page.getRight();
|
||||
|
||||
try {
|
||||
// 构造请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("file", base64);
|
||||
requestBody.put("fileType", scannedPdf ? 1 : 0); // 文件类型。0表示PDF文件,1表示图像文件
|
||||
HttpRequest post = HttpUtil.createPost(orcServerUrl);
|
||||
post.header("Content-Type", "application/json");
|
||||
post.body(JSONObject.toJSONString(requestBody));
|
||||
HttpResponse response = post.execute();
|
||||
|
||||
JSONObject jsonObject = JSONObject.parseObject(response.body());
|
||||
if (jsonObject.containsKey("errorCode") && jsonObject.getIntValue("errorCode") == 500) {
|
||||
log.error("请求失败,错误码:{},错误信息:{}", jsonObject.getIntValue("errorCode"), jsonObject.getString("errorMsg"));
|
||||
// 请求失败,为每一页添加空字符串
|
||||
for (int j = 0; j < pageData; j++) {
|
||||
contentList.add("");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
JSONObject result = jsonObject.getJSONObject("result");
|
||||
JSONArray layoutParsingResults = result.getJSONArray("layoutParsingResults");
|
||||
if (layoutParsingResults == null || layoutParsingResults.isEmpty()) {
|
||||
// 没有识别结果,为每一页添加空字符串
|
||||
for (int j = 0; j < pageData; j++) {
|
||||
contentList.add("");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 处理每一页的识别结果
|
||||
for (int j = 0; j < layoutParsingResults.size(); j++) {
|
||||
JSONObject layoutParsingResult = layoutParsingResults.getJSONObject(j);
|
||||
JSONObject markdown = layoutParsingResult.getJSONObject("markdown");
|
||||
if (markdown == null) {
|
||||
contentList.add("");
|
||||
continue;
|
||||
}
|
||||
String text = markdown.getString("text");
|
||||
if (StringUtils.isBlank(text)) {
|
||||
contentList.add("");
|
||||
continue;
|
||||
}
|
||||
contentList.add(text);
|
||||
}
|
||||
// 如果返回的页数少于预期页数,补充空字符串
|
||||
if (layoutParsingResults.size() < pageData) {
|
||||
for (int j = layoutParsingResults.size(); j < pageData; j++) {
|
||||
contentList.add("");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取页面 {} 内容失败", pageNum, e);
|
||||
// 出错时为每一页添加空字符串保持页数一致
|
||||
for (int j = 0; j < pageData; j++) {
|
||||
contentList.add("");
|
||||
}
|
||||
}
|
||||
}
|
||||
return contentList;
|
||||
}
|
||||
|
||||
// 获取所有页面内容
|
||||
public String extractImgPagesContent(File imgFile) throws Exception {
|
||||
try {
|
||||
byte[] pageBytes = Files.readAllBytes(imgFile.toPath());
|
||||
String base64 = Base64.getEncoder().encodeToString(pageBytes);
|
||||
if (StrUtil.isBlank(base64)) {
|
||||
throw new ServiceException("无法获取图片内容");
|
||||
}
|
||||
Files.deleteIfExists(imgFile.toPath()); // 清理文件
|
||||
|
||||
// 构造请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("file", base64);
|
||||
requestBody.put("fileType", 1); // 文件类型。0表示PDF文件,1表示图像文件
|
||||
HttpRequest post = HttpUtil.createPost(orcServerUrl);
|
||||
post.header("Content-Type", "application/json");
|
||||
post.body(JSONObject.toJSONString(requestBody));
|
||||
HttpResponse response = post.execute();
|
||||
|
||||
JSONObject jsonObject = JSONObject.parseObject(response.body());
|
||||
if (jsonObject.containsKey("errorCode") && jsonObject.getIntValue("errorCode") == 500) {
|
||||
log.error("请求失败,错误码:{},错误信息:{}", jsonObject.getIntValue("errorCode"), jsonObject.getString("errorMsg"));
|
||||
return "";
|
||||
}
|
||||
JSONObject result = jsonObject.getJSONObject("result");
|
||||
JSONArray layoutParsingResults = result.getJSONArray("layoutParsingResults");
|
||||
if (layoutParsingResults == null || layoutParsingResults.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
JSONObject layoutParsingResults_0 = layoutParsingResults.getJSONObject(0);
|
||||
JSONObject markdown = layoutParsingResults_0.getJSONObject("markdown");
|
||||
if (markdown == null) {
|
||||
return "";
|
||||
}
|
||||
String text = markdown.getString("text");
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return "";
|
||||
}
|
||||
return text;
|
||||
} catch (Exception e) {
|
||||
log.error("获取图片内容失败", e);
|
||||
throw new ServiceException("获取图片内容失败");
|
||||
} finally {
|
||||
try {
|
||||
if (imgFile != null && imgFile.exists()) {
|
||||
Files.deleteIfExists(imgFile.toPath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("删除文件失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* L base64 M 临时文件 R 是否内容为图片
|
||||
* @param pdfFile
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private static List<Triple<String, Integer, Boolean>> splitPdfToBase64Pages(File pdfFile) throws IOException {
|
||||
List<Triple<String, Integer, Boolean>> base64Pages = new ArrayList<>();
|
||||
try (PDDocument document = Loader.loadPDF(pdfFile)) {
|
||||
int pageCount = document.getNumberOfPages();
|
||||
// 每10页为一组进行切割
|
||||
for (int startPage = 0; startPage < pageCount; startPage += 10) {
|
||||
int endPage = Math.min(startPage + 10, pageCount);
|
||||
PDDocument pageDoc = new PDDocument();
|
||||
|
||||
// 添加10页内容到新文档
|
||||
for (int i = startPage; i < endPage; i++) {
|
||||
PDPage page = document.getPage(i);
|
||||
pageDoc.addPage(page);
|
||||
}
|
||||
|
||||
// 使用 try-with-resources 确保临时文件被正确处理
|
||||
File tempPageFile = null;
|
||||
try {
|
||||
// 创建唯一的临时文件
|
||||
tempPageFile = File.createTempFile("page_" + System.currentTimeMillis() + "_" + startPage, ".pdf");
|
||||
|
||||
// 保存页面到临时文件
|
||||
pageDoc.save(tempPageFile);
|
||||
|
||||
// 转换为 Base64
|
||||
byte[] pageBytes = Files.readAllBytes(tempPageFile.toPath());
|
||||
String base64 = Base64.getEncoder().encodeToString(pageBytes);
|
||||
|
||||
// 判断是否扫描件
|
||||
boolean scannedPdf = true;
|
||||
try (PDDocument temp = Loader.loadPDF(tempPageFile)) {
|
||||
PDFTextStripper stripper = new PDFTextStripper();
|
||||
String text = stripper.getText(temp);
|
||||
if (text != null && !text.trim().isEmpty()) {
|
||||
scannedPdf = false;
|
||||
}
|
||||
}
|
||||
// 中间字段记录这组包含的页数
|
||||
int pagesInGroup = endPage - startPage;
|
||||
base64Pages.add(ImmutableTriple.of(base64, pagesInGroup, scannedPdf));
|
||||
|
||||
} finally {
|
||||
// 确保在任何情况下都关闭并删除临时文件
|
||||
pageDoc.close();
|
||||
if (tempPageFile != null && tempPageFile.exists()) {
|
||||
Files.deleteIfExists(tempPageFile.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return base64Pages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 提取每页的摘要信息(指纹)
|
||||
*/
|
||||
public class PageFingerprinter {
|
||||
public static List<String> extractPageSummaries(PDDocument document) throws IOException, IOException {
|
||||
List<String> summaries = new ArrayList<>();
|
||||
for (int i = 0; i < document.getNumberOfPages(); i++) {
|
||||
PDPage page = document.getPage(i);
|
||||
PDFTextStripper stripper = new PDFTextStripper();
|
||||
stripper.setStartPage(i + 1);
|
||||
stripper.setEndPage(i + 1);
|
||||
String text = stripper.getText(document);
|
||||
// 去掉回车符、空格、换行符、换页符、缩进符、'\r'
|
||||
text = text.replaceAll("\\s+", "");
|
||||
// 取前 100 字作为指纹(可根据需要扩展)
|
||||
//String summary = text.length() > 500 ? text.substring(0, 500) : text;
|
||||
summaries.add(text);
|
||||
}
|
||||
return summaries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
import com.github.difflib.DiffUtils;
|
||||
import com.github.difflib.patch.AbstractDelta;
|
||||
import com.github.difflib.patch.DeltaType;
|
||||
import com.github.difflib.patch.Patch;
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import com.qingyun.service.utils.PdfUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class PdfDiffWithPosition {
|
||||
|
||||
private final PdfUtil pdfUtil;
|
||||
|
||||
public List<DiffRegion> compare(ContractCompareTask contractCompareTask) {
|
||||
File tempFile1 = null;
|
||||
File tempFile2 = null;
|
||||
try {
|
||||
tempFile1 = pdfUtil.downloadFile(contractCompareTask.getFirstContractUrl(), contractCompareTask.getFirstContractName());
|
||||
tempFile2 = pdfUtil.downloadFile(contractCompareTask.getSecondContractUrl(), contractCompareTask.getSecondContractName());
|
||||
//PDDocument doc1 = Loader.loadPDF(tempFile1);
|
||||
//PDDocument doc2 = Loader.loadPDF(tempFile2);
|
||||
|
||||
// 提取摘要
|
||||
// List<String> summary1 = PageFingerprinter.extractPageSummaries(doc1);
|
||||
// List<String> summary2 = PageFingerprinter.extractPageSummaries(doc2);
|
||||
//
|
||||
// // 判断是否可比
|
||||
// if (!DocumentStructureComparator.isComparable(summary1, summary2)) {
|
||||
// throw new ServiceException("文档结构差异过大,不适合直接比对!");
|
||||
// }
|
||||
|
||||
// 1. 加载文档并提取带位置信息的文本
|
||||
List<TextElement> textElements1 = extractTextWithPosition(tempFile1);
|
||||
List<TextElement> textElements2 = extractTextWithPosition(tempFile2);
|
||||
|
||||
// 新:词级
|
||||
List<TokenWithPosition> tokens1 = extractTokens(textElements1);
|
||||
List<TokenWithPosition> tokens2 = extractTokens(textElements2);
|
||||
|
||||
List<String> words1 = new ArrayList<>();
|
||||
List<String> words2 = new ArrayList<>();
|
||||
|
||||
for (TokenWithPosition t : tokens1) words1.add(t.token);
|
||||
for (TokenWithPosition t : tokens2) words2.add(t.token);
|
||||
|
||||
// 3. 执行差异检测
|
||||
Patch<String> patch = DiffUtils.diff(words1, words2);
|
||||
|
||||
// 4. 处理差异并记录位置信息
|
||||
List<ChangeLocation> mergedChanges = new ArrayList<>();
|
||||
ChangeLocation currentMerge = null;
|
||||
|
||||
for (AbstractDelta<String> delta : patch.getDeltas()) {
|
||||
|
||||
if (delta.getType() == DeltaType.CHANGE) {
|
||||
List<TextElement> sourceElements = mapTokensToElements(
|
||||
tokens1, delta.getSource().getPosition(),
|
||||
delta.getSource().getLines().size(), textElements1);
|
||||
List<TextElement> targetElements = mapTokensToElements(
|
||||
tokens2, delta.getTarget().getPosition(),
|
||||
delta.getTarget().getLines().size(), textElements2);
|
||||
|
||||
if (currentMerge == null) {
|
||||
// 开始新的合并
|
||||
currentMerge = new ChangeLocation(
|
||||
delta.getSource().getLines(),
|
||||
delta.getTarget().getLines(),
|
||||
DeltaType.CHANGE,
|
||||
sourceElements,
|
||||
targetElements
|
||||
);
|
||||
} else if (canMerge(currentMerge, sourceElements, targetElements)) {
|
||||
// 合并到当前记录
|
||||
currentMerge.sourceText.addAll(delta.getSource().getLines());
|
||||
currentMerge.targetText.addAll(delta.getTarget().getLines());
|
||||
currentMerge.sourceElements.addAll(sourceElements);
|
||||
currentMerge.targetElements.addAll(targetElements);
|
||||
} else {
|
||||
// 保存当前合并并开始新记录
|
||||
mergedChanges.add(currentMerge);
|
||||
currentMerge = new ChangeLocation(
|
||||
delta.getSource().getLines(),
|
||||
delta.getTarget().getLines(),
|
||||
DeltaType.CHANGE,
|
||||
sourceElements,
|
||||
targetElements
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 非 CHANGE 类型直接添加
|
||||
if (currentMerge != null) {
|
||||
mergedChanges.add(currentMerge);
|
||||
currentMerge = null;
|
||||
}
|
||||
mergedChanges.add(new ChangeLocation(
|
||||
delta.getSource().getLines(),
|
||||
delta.getTarget().getLines(),
|
||||
delta.getType(),
|
||||
mapTokensToElements(tokens1, delta.getSource().getPosition(),
|
||||
delta.getSource().getLines().size(), textElements1),
|
||||
mapTokensToElements(tokens2, delta.getTarget().getPosition(),
|
||||
delta.getTarget().getLines().size(), textElements2)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 添加剩余的合并记录
|
||||
if (currentMerge != null) {
|
||||
mergedChanges.add(currentMerge);
|
||||
}
|
||||
|
||||
// 5. 输出差异位置信息(供前端使用)
|
||||
List<DiffRegion> diffRegions = new ArrayList<>();
|
||||
for (ChangeLocation change : mergedChanges) {
|
||||
if (isMeaninglessChange(change.sourceText) && isMeaninglessChange(change.targetText)) {
|
||||
continue; // 忽略无意义的变化
|
||||
}
|
||||
|
||||
DiffRegion region = new DiffRegion();
|
||||
region.changeType = change.changeType.name();
|
||||
|
||||
// 拼接文本内容
|
||||
if (!change.sourceText.isEmpty()) {
|
||||
region.sourceText = String.join("", change.sourceText);
|
||||
}
|
||||
if (!change.targetText.isEmpty()) {
|
||||
region.targetText = String.join("", change.targetText);
|
||||
}
|
||||
|
||||
// 源文档的包围框
|
||||
if (!change.sourceElements.isEmpty()) {
|
||||
TextElement firstSource = change.sourceElements.get(0);
|
||||
TextElement lastSource = change.sourceElements.get(change.sourceElements.size() - 1);
|
||||
region.sourceBox = new BoundingBox();
|
||||
region.sourceBox.x = firstSource.x;
|
||||
region.sourceBox.y = firstSource.y;
|
||||
region.sourceBox.width = lastSource.x + lastSource.width - firstSource.x;
|
||||
region.sourceBox.height = firstSource.height;
|
||||
region.sourcePage = firstSource.pageNumber;
|
||||
}
|
||||
|
||||
// 目标文档的包围框
|
||||
if (!change.targetElements.isEmpty()) {
|
||||
TextElement firstTarget = change.targetElements.get(0);
|
||||
TextElement lastTarget = change.targetElements.get(change.targetElements.size() - 1);
|
||||
region.targetBox = new BoundingBox();
|
||||
region.targetBox.x = firstTarget.x;
|
||||
region.targetBox.y = firstTarget.y;
|
||||
region.targetBox.width = lastTarget.x + lastTarget.width - firstTarget.x;
|
||||
region.targetBox.height = firstTarget.height;
|
||||
region.targetPage = firstTarget.pageNumber;
|
||||
}
|
||||
|
||||
diffRegions.add(region);
|
||||
}
|
||||
|
||||
return diffRegions;
|
||||
} catch (IOException e) {
|
||||
log.error("PDF 文件读取失败", e);
|
||||
throw new ServiceException("PDF文件读取失败");
|
||||
} catch (ServiceException e) {
|
||||
log.error(e.getMessage());
|
||||
throw e; // Rethrow the exception
|
||||
} finally {
|
||||
if (tempFile1 != null && tempFile1.exists() ) {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile1.toPath());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
if (tempFile2 != null && tempFile2.exists()) {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile2.toPath());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 提取带位置信息的文本
|
||||
private List<TextElement> extractTextWithPosition(File pdfFile) throws IOException {
|
||||
try (PDDocument document = Loader.loadPDF(pdfFile)) {
|
||||
PositionTextStripper stripper = new PositionTextStripper();
|
||||
stripper.setSortByPosition(true);
|
||||
stripper.setStartPage(1);
|
||||
stripper.setEndPage(document.getNumberOfPages());
|
||||
stripper.getText(document);
|
||||
return stripper.getTextElements();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并判断方法
|
||||
*
|
||||
* @param prev
|
||||
* @param sourceElements
|
||||
* @param targetElements
|
||||
* @return
|
||||
*/
|
||||
private boolean canMerge(ChangeLocation prev, List<TextElement> sourceElements, List<TextElement> targetElements) {
|
||||
if (prev.sourceElements.isEmpty() || prev.targetElements.isEmpty() || sourceElements.isEmpty() || targetElements.isEmpty())
|
||||
return false;
|
||||
|
||||
TextElement prevSourceLast = prev.sourceElements.get(prev.sourceElements.size() - 1);
|
||||
TextElement currSourceFirst = sourceElements.get(0);
|
||||
|
||||
TextElement prevTargetLast = prev.targetElements.get(prev.targetElements.size() - 1);
|
||||
TextElement currTargetFirst = targetElements.get(0);
|
||||
|
||||
boolean sourceContinuous = isContinuous(prevSourceLast, currSourceFirst);
|
||||
boolean targetContinuous = isContinuous(prevTargetLast, currTargetFirst);
|
||||
|
||||
return sourceContinuous && targetContinuous;
|
||||
}
|
||||
|
||||
private boolean isContinuous(TextElement prev, TextElement curr) {
|
||||
return prev.pageNumber == curr.pageNumber &&
|
||||
Math.abs(prev.y - curr.y) < 2 &&
|
||||
(curr.x - (prev.x + prev.width)) < 5;
|
||||
}
|
||||
|
||||
private boolean isMeaninglessChange(List<String> textList) {
|
||||
return textList.stream().allMatch(s -> s.trim().isEmpty());
|
||||
}
|
||||
|
||||
private List<TextElement> mapTokensToElements(
|
||||
List<TokenWithPosition> tokens, int start, int size,
|
||||
List<TextElement> elements) {
|
||||
|
||||
if (start < 0 || size <= 0 || start >= tokens.size()) return new ArrayList<>();
|
||||
|
||||
int end = Math.min(start + size, tokens.size());
|
||||
List<TextElement> result = new ArrayList<>();
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
TokenWithPosition t = tokens.get(i);
|
||||
// 找到包含该 token 的 TextElement(简单匹配页码 + 坐标重叠)
|
||||
for (TextElement e : elements) {
|
||||
if (e.pageNumber == t.page &&
|
||||
e.x <= t.x && (e.x + e.width) >= (t.x + t.width) &&
|
||||
Math.abs(e.y - t.y) < 1) {
|
||||
result.add(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private List<TokenWithPosition> extractTokens(List<TextElement> elements) {
|
||||
List<TokenWithPosition> tokens = new ArrayList<>();
|
||||
int globalIdx = 0;
|
||||
|
||||
for (TextElement e : elements) {
|
||||
String text = e.text;
|
||||
float charWidth = e.width / Math.max(text.length(), 1);
|
||||
float offsetX = 0;
|
||||
|
||||
Pattern pattern = Pattern.compile("\\S+|\\s+");
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
while (matcher.find()) {
|
||||
String token = matcher.group();
|
||||
float tokenWidth = token.length() * charWidth;
|
||||
tokens.add(new TokenWithPosition(
|
||||
token,
|
||||
e.pageNumber,
|
||||
e.x + offsetX,
|
||||
e.y,
|
||||
tokenWidth,
|
||||
e.height,
|
||||
globalIdx++
|
||||
));
|
||||
offsetX += tokenWidth;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class PdfScanChecker {
|
||||
public static boolean isScannedPdf(PDDocument document) throws IOException {
|
||||
PDFTextStripper stripper = new PDFTextStripper();
|
||||
for (int i = 0; i < document.getNumberOfPages(); i++) {
|
||||
String text = stripper.getText(document);
|
||||
if (text != null && !text.trim().isEmpty()) {
|
||||
// 如果能提取出文字,说明不是纯扫描件
|
||||
document.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
document.close();
|
||||
// 如果所有页面都提取不出文字,可能是扫描件
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 自定义文本提取器(收集位置信息)
|
||||
*/
|
||||
public class PositionTextStripper extends PDFTextStripper {
|
||||
private final List<TextElement> textElements = new ArrayList<>();
|
||||
private int currentPage = 1;
|
||||
|
||||
public PositionTextStripper() throws IOException {
|
||||
super();
|
||||
super.setSortByPosition(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startPage(PDPage page) {
|
||||
currentPage = getCurrentPageNo();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeString(String text, List<TextPosition> textPositions) {
|
||||
for (TextPosition position : textPositions) {
|
||||
textElements.add(new TextElement(position, currentPage));
|
||||
}
|
||||
}
|
||||
|
||||
public List<TextElement> getTextElements() {
|
||||
return textElements;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
/**
|
||||
* 文本元素类(存储位置信息)
|
||||
*/
|
||||
public class TextElement {
|
||||
|
||||
public String text;
|
||||
public int pageNumber;
|
||||
public float x, y, width, height;
|
||||
|
||||
TextPosition position;
|
||||
|
||||
TextElement(TextPosition position, int pageNumber) {
|
||||
this.text = position.getUnicode();
|
||||
this.pageNumber = pageNumber;
|
||||
this.x = position.getX();
|
||||
this.y = position.getY();
|
||||
this.width = position.getWidth();
|
||||
this.height = position.getHeight();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.qingyun.service.compare;
|
||||
|
||||
/**
|
||||
* 词级单元
|
||||
*/
|
||||
public class TokenWithPosition {
|
||||
|
||||
String token; // 词内容
|
||||
int page; // 页码
|
||||
float x, y, width, height; // 坐标
|
||||
int globalIndex; // 在整个 token 列表中的索引,用于定位
|
||||
|
||||
TokenWithPosition(String token, int page, float x, float y, float w, float h, int idx) {
|
||||
this.token = token;
|
||||
this.page = page;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
this.globalIndex = idx;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.qingyun.service.compare.factory;
|
||||
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import com.qingyun.service.compare.strategy.FileComparisonStrategy;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 策略工厂(根据文件类型选择策略)
|
||||
*/
|
||||
@Component
|
||||
public class FileComparisonStrategyFactory {
|
||||
|
||||
private final List<FileComparisonStrategy> strategies;
|
||||
|
||||
public FileComparisonStrategyFactory(List<FileComparisonStrategy> strategies) {
|
||||
this.strategies = strategies.stream()
|
||||
.sorted(Comparator.comparing(strategy -> {
|
||||
Order order = strategy.getClass().getAnnotation(Order.class);
|
||||
return order != null ? order.value() : Integer.MAX_VALUE;
|
||||
}))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据任务中的文件扩展名选择合适的策略
|
||||
* @param task
|
||||
* @return
|
||||
*/
|
||||
public FileComparisonStrategy getStrategy(ContractCompareTask task) {
|
||||
String ext1 = getFileExtension(task.getFirstContractUrl());
|
||||
String ext2 = getFileExtension(task.getSecondContractUrl());
|
||||
|
||||
if (!ext1.equalsIgnoreCase(ext2)) {
|
||||
throw new ServiceException("两份文件类型不一致,无法对比");
|
||||
}
|
||||
|
||||
for (FileComparisonStrategy strategy : strategies) {
|
||||
if (strategy.supports(ext1)) {
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
throw new ServiceException("不支持的文件类型: " + ext1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
* @param fileName 文件名
|
||||
* @return 文件扩展名
|
||||
*/
|
||||
private String getFileExtension(String fileName) {
|
||||
if (fileName == null || fileName.lastIndexOf('.') == -1) return "";
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package com.qingyun.service.compare.service;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.agentsflex.core.llm.response.AiMessageResponse;
|
||||
import com.agentsflex.core.message.AiMessage;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import com.qingyun.service.agent.LlmStrategy;
|
||||
import com.qingyun.service.agent.LlmStrategyFactory;
|
||||
import com.qingyun.service.agent.TemplateManager;
|
||||
import com.qingyun.service.compare.FetchAllPagesContent;
|
||||
import com.qingyun.service.domain.ContractModelConfig;
|
||||
import com.qingyun.service.domain.ContractReviewResult;
|
||||
import com.qingyun.service.domain.ContractReviewTask;
|
||||
import com.qingyun.service.mapper.ContractModelConfigMapper;
|
||||
import com.qingyun.service.mapper.ContractReviewResultMapper;
|
||||
import com.qingyun.service.mapper.ContractReviewTaskMapper;
|
||||
import com.qingyun.service.utils.MarkdownUtils;
|
||||
import com.qingyun.service.utils.PdfUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutableTriple;
|
||||
import org.apache.commons.lang3.tuple.Triple;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@SuppressWarnings("all")
|
||||
public class ReviewTaskService {
|
||||
|
||||
@Autowired
|
||||
private ContractReviewTaskMapper baseMapper;
|
||||
|
||||
@Autowired
|
||||
private ContractReviewResultMapper resultBaseMapper;
|
||||
|
||||
@Autowired
|
||||
private ContractModelConfigMapper modelBaseMapper;
|
||||
|
||||
@Autowired
|
||||
private PdfUtil pdfUtil;
|
||||
|
||||
@Autowired
|
||||
private FetchAllPagesContent fetchAllPagesContent;
|
||||
|
||||
// 使用Semaphore控制并发数
|
||||
private static final Semaphore semaphore = new Semaphore(5);
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void execute(ContractReviewTask contractReviewTask, Long modelId) {
|
||||
ExecutorService executor = null;
|
||||
try {
|
||||
String contractUrl = contractReviewTask.getContractUrl();
|
||||
String reviewElement = contractReviewTask.getReviewElement();
|
||||
String reviewRule = contractReviewTask.getReviewRule();
|
||||
|
||||
String rulePromptTemp = TemplateManager.getTemplate("classpath:prompt/contractReview.txt");
|
||||
String elementPromptTemp = TemplateManager.getTemplate("classpath:prompt/reviewElement.txt");
|
||||
|
||||
ContractModelConfig contractModelConfig = null;
|
||||
if (modelId != null) {
|
||||
contractModelConfig = modelBaseMapper.selectById(contractReviewTask.getModelId());
|
||||
}
|
||||
LlmStrategy strategy = LlmStrategyFactory.getStrategy(contractModelConfig);
|
||||
// 1. 并发获取所有页面内容
|
||||
String contractContentMd = contractReviewTask.getContractContentMd();
|
||||
if (StringUtils.isBlank(contractContentMd)) {
|
||||
parsePdf(contractReviewTask);
|
||||
contractContentMd = contractReviewTask.getContractContentMd();
|
||||
}
|
||||
|
||||
// 获取所有页面内容, 数组索引为页码,从0开始
|
||||
List<String> pagesContent = JSONArray.parseArray(contractReviewTask.getContractContentMd(), String.class);
|
||||
if (CollectionUtil.isEmpty(pagesContent)) {
|
||||
log.error("合同内容为空,无法解析");
|
||||
contractReviewTask.setStatus(4);
|
||||
contractReviewTask.setErrorMsg("合同内容为空");
|
||||
baseMapper.updateById(contractReviewTask);
|
||||
return;
|
||||
}
|
||||
|
||||
int totalPages = pagesContent.size();
|
||||
|
||||
// 将页面分组:每3页一组
|
||||
List<List<String>> pageGroups = new ArrayList<>();
|
||||
List<List<Integer>> pageNumbers = new ArrayList<>(); // 记录每组对应的页码(从1开始)
|
||||
|
||||
for (int i = 0; i < totalPages; i += 3) {
|
||||
List<String> group = new ArrayList<>();
|
||||
List<Integer> nums = new ArrayList<>();
|
||||
for (int j = i; j < i + 3 && j < totalPages; j++) {
|
||||
group.add(pagesContent.get(j));
|
||||
nums.add(j + 1); // 页码从1开始
|
||||
}
|
||||
pageGroups.add(group);
|
||||
pageNumbers.add(nums);
|
||||
}
|
||||
List<ContractReviewResult> allResults = new ArrayList<>();
|
||||
// 并发处理每组
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < pageGroups.size(); i++) {
|
||||
List<String> group = pageGroups.get(i);
|
||||
List<Integer> pageNums = pageNumbers.get(i);
|
||||
|
||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
semaphore.acquire(); // 获取信号量
|
||||
|
||||
try {
|
||||
// 构建 JSON 格式的上下文
|
||||
JSONArray contextArray = new JSONArray();
|
||||
for (int idx = 0; idx < group.size(); idx++) {
|
||||
JSONObject item = new JSONObject();
|
||||
item.put("pageNum", pageNums.get(idx));
|
||||
item.put("context", StringUtils.defaultString(group.get(idx)));
|
||||
contextArray.add(item);
|
||||
}
|
||||
String fullContextJson = contextArray.toJSONString();
|
||||
|
||||
if (StringUtils.isBlank(fullContextJson)) {
|
||||
log.warn("分组页码 {} 内容为空,跳过", pageNums);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建参数
|
||||
Map<String, String> elementParams = new HashMap<>();
|
||||
elementParams.put("rules", reviewElement);
|
||||
elementParams.put("context", fullContextJson);
|
||||
|
||||
Map<String, String> ruleParams = new HashMap<>();
|
||||
ruleParams.put("rules", reviewRule);
|
||||
ruleParams.put("context", fullContextJson);
|
||||
|
||||
AiMessageResponse elementResponse = null;
|
||||
AiMessageResponse ruleResponse = null;
|
||||
|
||||
if (StringUtils.isNotBlank(reviewElement)) {
|
||||
elementResponse = strategy.chat(elementPromptTemp, elementParams);
|
||||
log.info("Element Response for pages {}: {}", pageNums, elementResponse);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(reviewRule)) {
|
||||
ruleResponse = strategy.chat(rulePromptTemp, ruleParams);
|
||||
log.info("Rule Response for pages {}: {}", pageNums, ruleResponse);
|
||||
}
|
||||
|
||||
// 处理响应
|
||||
synchronized (allResults) {
|
||||
processResponses(elementResponse, ruleResponse, contractReviewTask, allResults);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理页码组 {} 时出错", pageNums, e);
|
||||
} finally {
|
||||
semaphore.release(); // 释放信号量
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("线程被中断", e);
|
||||
}
|
||||
});
|
||||
|
||||
futures.add(future);
|
||||
}
|
||||
// 等待所有完成
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
|
||||
// === 所有处理完成,现在统一入库 ===
|
||||
if (!allResults.isEmpty()) {
|
||||
resultBaseMapper.insertBatch(allResults);
|
||||
}
|
||||
|
||||
contractReviewTask.setErrorMsg("执行成功");
|
||||
contractReviewTask.setStatus(3); // 成功
|
||||
} catch (ServiceException e) {
|
||||
log.error("执行任务失败", e.getMessage());
|
||||
contractReviewTask.setStatus(4);
|
||||
contractReviewTask.setErrorMsg(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("contractReviewTask.{} 执行任务失败: {}", contractReviewTask.getTaskId(), e);
|
||||
contractReviewTask.setStatus(4);
|
||||
contractReviewTask.setErrorMsg("执行任务失败");
|
||||
|
||||
} finally {
|
||||
if (executor != null) {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
baseMapper.updateById(contractReviewTask);
|
||||
}
|
||||
|
||||
// 修改原有的 fetchAllPagesContent 方法以复用新方法
|
||||
private List<String> fetchAllPagesContent(String contractUrl, String contractName) throws Exception {
|
||||
File pdfFile = pdfUtil.downloadFile(contractUrl, contractName);
|
||||
try {
|
||||
return fetchAllPagesContent.extractPDFPagesContent(pdfFile);
|
||||
} finally {
|
||||
// 清理下载的原始文件
|
||||
Files.deleteIfExists(pdfFile.toPath());
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前页的前一页末尾内容
|
||||
private String getPrevContext(List<String> pagesContent, int pageNum) {
|
||||
if (pageNum <= 0 || pageNum >= pagesContent.size()) return "";
|
||||
String prevContent = pagesContent.get(pageNum - 1);
|
||||
if (prevContent == null || prevContent.isEmpty()) return "";
|
||||
return prevContent.length() > 20 ? prevContent.substring(prevContent.length() - 20) : prevContent;
|
||||
}
|
||||
|
||||
// 获取当前页的后一页开头内容
|
||||
private String getNextContext(List<String> pagesContent, int pageNum) {
|
||||
if (pageNum >= pagesContent.size() - 1) return "";
|
||||
String nextContent = pagesContent.get(pageNum + 1);
|
||||
if (nextContent == null || nextContent.isEmpty()) return "";
|
||||
return nextContent.length() > 20 ? nextContent.substring(0, 20) : nextContent;
|
||||
}
|
||||
|
||||
// 解析保存审查结果
|
||||
private void processResponses(AiMessageResponse elementResponse,
|
||||
AiMessageResponse ruleResponse,
|
||||
ContractReviewTask task,
|
||||
List<ContractReviewResult> results) {
|
||||
processResponse(ruleResponse, task, 1, results);
|
||||
processResponse(elementResponse, task, 2, results);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个响应并保存审查结果
|
||||
* [
|
||||
* {
|
||||
* "pageNum": "页码",
|
||||
* "contractContent": "原文内容",
|
||||
* "reviewBasis": "审查判断依据",
|
||||
* "riskTips": "审查异常风险提示",
|
||||
* "riskLevel": "无风险",
|
||||
* "modifyExample": "建议修改示例"
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
private void processResponse(AiMessageResponse response, ContractReviewTask task, Integer specType, List<ContractReviewResult> results) {
|
||||
Optional.ofNullable(response)
|
||||
.map(AiMessageResponse::getMessage)
|
||||
.map(AiMessage::getContent)
|
||||
.filter(content -> !content.isEmpty())
|
||||
.ifPresent(content -> {
|
||||
// 移除 <think> 标签及其内容
|
||||
String cleanedContent = content.replaceAll("(?s)<think>.*?</think>", "");
|
||||
|
||||
// 提取所有 JSON block
|
||||
List<String> jsonBlocks = MarkdownUtils.extractJsonBlocks(cleanedContent);
|
||||
if (jsonBlocks == null || jsonBlocks.isEmpty()) {
|
||||
log.warn("未提取到有效的 JSON 内容,响应内容: {}", cleanedContent.substring(0, Math.min(200, cleanedContent.length())));
|
||||
return;
|
||||
}
|
||||
String firstBlock = jsonBlocks.get(0);
|
||||
try {
|
||||
List<ContractReviewResult> resultList = JSONArray.parseArray(firstBlock, ContractReviewResult.class);
|
||||
if (resultList != null && !resultList.isEmpty()) {
|
||||
resultList.forEach(result -> {
|
||||
result.setTaskId(task.getTaskId());
|
||||
result.setSpecType(specType);
|
||||
results.add(result);
|
||||
});
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析 JSON 失败", e);
|
||||
e.printStackTrace(); // JSON 解析失败处理
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* L base64 M 临时文件 R 是否内容为图片
|
||||
* @param pdfFile
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private List<Triple<String, Object, Boolean>> splitPdfToBase64Pages(File pdfFile) throws IOException {
|
||||
List<Triple<String, Object, Boolean>> base64Pages = new ArrayList<>();
|
||||
try (PDDocument document = Loader.loadPDF(pdfFile)) {
|
||||
int pageCount = document.getNumberOfPages();
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
PDDocument pageDoc = new PDDocument();
|
||||
PDPage page = document.getPage(i);
|
||||
pageDoc.addPage(page);
|
||||
|
||||
// 使用 try-with-resources 确保临时文件被正确处理
|
||||
File tempPageFile = null;
|
||||
try {
|
||||
// 创建唯一的临时文件
|
||||
tempPageFile = File.createTempFile("page_" + System.currentTimeMillis() + "_" + i, ".pdf");
|
||||
|
||||
// 保存页面到临时文件
|
||||
pageDoc.save(tempPageFile);
|
||||
|
||||
// 转换为 Base64
|
||||
byte[] pageBytes = Files.readAllBytes(tempPageFile.toPath());
|
||||
String base64 = Base64.getEncoder().encodeToString(pageBytes);
|
||||
|
||||
// 判断是否扫描件
|
||||
boolean scannedPdf = true;
|
||||
try (PDDocument temp = Loader.loadPDF(tempPageFile)) {
|
||||
PDFTextStripper stripper = new PDFTextStripper();
|
||||
String text = stripper.getText(temp);
|
||||
if (text != null && !text.trim().isEmpty()) {
|
||||
scannedPdf = false;
|
||||
}
|
||||
}
|
||||
|
||||
base64Pages.add(ImmutableTriple.of(base64, null, scannedPdf));
|
||||
|
||||
} finally {
|
||||
// 确保在任何情况下都关闭并删除临时文件
|
||||
pageDoc.close();
|
||||
if (tempPageFile != null && tempPageFile.exists()) {
|
||||
Files.deleteIfExists(tempPageFile.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return base64Pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析PDF文件 转成成MD格式
|
||||
* @param contractReviewTask 合同审查任务对象
|
||||
*/
|
||||
//@Async
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void parsePdf(ContractReviewTask contractReviewTask) {
|
||||
File originalFile = null;
|
||||
File pdfFile = null;
|
||||
try {
|
||||
String contractUrl = contractReviewTask.getContractUrl();
|
||||
String contractName = contractReviewTask.getContractName();
|
||||
|
||||
// 下载原始文件
|
||||
originalFile = pdfUtil.downloadFile(contractUrl, contractName);
|
||||
|
||||
// 获取所有页面内容 MD格式
|
||||
List<String> pagesContent = fetchAllPagesContent.extractPDFPagesContent(originalFile);
|
||||
if (pagesContent != null && !pagesContent.isEmpty()) {
|
||||
contractReviewTask.setContractContentMd(JSONArray.toJSONString(pagesContent));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析文件失败: {}", e.getMessage(), e);
|
||||
contractReviewTask.setStatus(4);
|
||||
throw new ServiceException("解析文件失败: " + e.getMessage());
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
try {
|
||||
if (originalFile != null && originalFile.exists()) {
|
||||
Files.deleteIfExists(originalFile.toPath());
|
||||
}
|
||||
if (pdfFile != null && pdfFile.exists() && !pdfFile.equals(originalFile)) {
|
||||
Files.deleteIfExists(pdfFile.toPath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("清理临时文件失败: {}", e.getMessage());
|
||||
}
|
||||
baseMapper.updateById(contractReviewTask);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
* @param fileName 文件名
|
||||
* @return 文件扩展名(包括点号)
|
||||
*/
|
||||
private String getFileExtension(String fileName) {
|
||||
if (fileName == null || fileName.lastIndexOf('.') == -1) {
|
||||
return ""; // 没有扩展名
|
||||
}
|
||||
return fileName.substring(fileName.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.qingyun.service.compare.strategy;
|
||||
|
||||
import com.qingyun.service.compare.DiffRegion;
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* 抽象模板类
|
||||
*/
|
||||
public abstract class AbstractFileComparisonStrategy implements FileComparisonStrategy {
|
||||
|
||||
protected Logger log = LoggerFactory.getLogger(getClass());
|
||||
|
||||
/**
|
||||
* 提取文件内容(如文本、结构等)
|
||||
*
|
||||
* @param task
|
||||
*/
|
||||
@Override
|
||||
public void extractContent(ContractCompareTask task) {
|
||||
doExtract(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行比对逻辑
|
||||
*
|
||||
* @param task
|
||||
* @return DiffRegion 列表
|
||||
*/
|
||||
@Override
|
||||
public List<DiffRegion> compare(ContractCompareTask task) {
|
||||
return doCompare(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装并保存比对结果
|
||||
*
|
||||
* @param task
|
||||
* @param diffRegions
|
||||
*/
|
||||
@Override
|
||||
public void assembleAndSaveResult(ContractCompareTask task, List<DiffRegion> diffRegions) {
|
||||
List<ContractCompareResult> results = new ArrayList<>();
|
||||
for (DiffRegion change : diffRegions) {
|
||||
results.add(buildResult(task, change));
|
||||
}
|
||||
saveResults(results);
|
||||
updateTaskStatus(task, 3, "任务执行完成");
|
||||
}
|
||||
|
||||
// 子类实现具体逻辑
|
||||
protected abstract void doExtract(ContractCompareTask task);
|
||||
protected abstract List<DiffRegion> doCompare(ContractCompareTask task);
|
||||
protected abstract ContractCompareResult buildResult(ContractCompareTask task, DiffRegion change);
|
||||
protected abstract void saveResults(List<ContractCompareResult> results);
|
||||
protected abstract void updateTaskStatus(ContractCompareTask task, int status, String msg);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.qingyun.service.compare.strategy;
|
||||
|
||||
import com.qingyun.service.compare.DiffRegion;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 统一的文件对比处理接口
|
||||
*/
|
||||
public interface FileComparisonStrategy {
|
||||
/**
|
||||
* 提取文件内容(如文本、结构等)
|
||||
*/
|
||||
void extractContent(ContractCompareTask task);
|
||||
|
||||
/**
|
||||
* 执行比对逻辑
|
||||
* @return DiffRegion 列表
|
||||
*/
|
||||
List<DiffRegion> compare(ContractCompareTask task);
|
||||
|
||||
/**
|
||||
* 组装并保存比对结果
|
||||
*/
|
||||
void assembleAndSaveResult(ContractCompareTask task, List<DiffRegion> diffRegions);
|
||||
|
||||
|
||||
boolean supports(String fileExtension); // 新增:判断是否支持该扩展名
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
package com.qingyun.service.compare.strategy;
|
||||
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.github.difflib.patch.DeltaType;
|
||||
import com.itextpdf.text.pdf.BaseFont;
|
||||
import com.qingyun.common.config.QingYunConfig;
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import com.qingyun.common.utils.DateUtils;
|
||||
import com.qingyun.common.utils.StringUtils;
|
||||
import com.qingyun.service.compare.*;
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import com.qingyun.service.mapper.ContractCompareResultMapper;
|
||||
import com.qingyun.service.mapper.ContractCompareTaskMapper;
|
||||
import com.qingyun.service.utils.PdfUtil;
|
||||
import com.vladsch.flexmark.html.HtmlRenderer;
|
||||
import com.vladsch.flexmark.parser.Parser;
|
||||
import com.vladsch.flexmark.profile.pegdown.Extensions;
|
||||
import com.vladsch.flexmark.util.ast.Node;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.xhtmlrenderer.pdf.ITextFontResolver;
|
||||
import org.xhtmlrenderer.pdf.ITextRenderer;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Component
|
||||
@Order(2)
|
||||
public class ImageComparisonStrategy extends AbstractFileComparisonStrategy implements Ordered {
|
||||
|
||||
@Autowired
|
||||
private ContractCompareResultMapper compareResultMapper;
|
||||
|
||||
@Autowired
|
||||
private ContractCompareTaskMapper contractCompareTaskMapper;
|
||||
|
||||
@Autowired
|
||||
private FetchAllPagesContent fetchAllPagesContent;
|
||||
|
||||
@Autowired
|
||||
private PdfDiffWithPosition pdfDiffWithPosition;
|
||||
|
||||
@Autowired
|
||||
private PdfUtil pdfUtil;
|
||||
|
||||
/**
|
||||
* 提取文件内容(如文本、结构等)
|
||||
* @param task 任务
|
||||
*/
|
||||
@Override
|
||||
protected void doExtract(ContractCompareTask task) {
|
||||
// 调用orc 获取md文本,保存为md文件
|
||||
try {
|
||||
File imgFile01 = pdfUtil.downloadFile(task.getFirstContractUrl(), UUID.randomUUID().toString() + ".png");
|
||||
String firstContent = fetchAllPagesContent.extractImgPagesContent(imgFile01);
|
||||
|
||||
File imgFile02 = pdfUtil.downloadFile(task.getSecondContractUrl(), UUID.randomUUID().toString() + ".png");
|
||||
String secondContent = fetchAllPagesContent.extractImgPagesContent(imgFile02);
|
||||
|
||||
// 将内容生成pdf文件
|
||||
if (StrUtil.isNotBlank(firstContent)) {
|
||||
List<String> firstContractLines = Arrays.asList(firstContent.split("\n"));
|
||||
// 去除空行
|
||||
firstContractLines = firstContractLines.stream().filter(StrUtil::isNotBlank).collect(Collectors.toList());
|
||||
firstContent = String.join("\n", firstContractLines);
|
||||
// 去除掉md文档的语法文字
|
||||
firstContent = removeMarkdownSyntax(firstContent);
|
||||
String newFilePath = writePdf(firstContent, task.getFirstContractName());
|
||||
task.setFirstContractUrl(newFilePath);
|
||||
task.setFirstContractContent(firstContent);
|
||||
// 改动文件名称,添加.md后缀
|
||||
String firstContractName = task.getFirstContractName();
|
||||
if (firstContractName.contains(".png") || firstContractName.contains(".jpg") || firstContractName.contains(".jpeg")) {
|
||||
String newName = firstContractName.substring(0, firstContractName.lastIndexOf(".")) + ".pdf";
|
||||
task.setFirstContractName(newName);
|
||||
}
|
||||
}
|
||||
if (StrUtil.isNotBlank(secondContent)) {
|
||||
List<String> secondContractLines = Arrays.asList(secondContent.split("\n"));
|
||||
// 去除空行
|
||||
secondContractLines = secondContractLines.stream().filter(StrUtil::isNotBlank).collect(Collectors.toList());
|
||||
secondContent = String.join("\n", secondContractLines);
|
||||
// 去除掉md文档的语法文字
|
||||
secondContent = removeMarkdownSyntax(secondContent);
|
||||
String newFilePath = writePdf(secondContent, task.getSecondContractName());
|
||||
task.setSecondContractUrl(newFilePath);
|
||||
task.setSecondContractContent(secondContent);
|
||||
String secondContractName = task.getSecondContractName();
|
||||
if (secondContractName.contains(".png") || secondContractName.contains(".jpg") || secondContractName.contains(".jpeg")) {
|
||||
String newName = secondContractName.substring(0, secondContractName.lastIndexOf(".")) + ".pdf";
|
||||
task.setSecondContractName(newName);
|
||||
}
|
||||
}
|
||||
|
||||
File firstFile = null;
|
||||
File secondFile = null;
|
||||
|
||||
firstFile = pdfUtil.downloadFile(task.getFirstContractUrl(), task.getFirstContractName());
|
||||
PDDocument firstDocument = Loader.loadPDF(firstFile);
|
||||
if (firstDocument == null) {
|
||||
throw new ServiceException("解析PDF文件失败");
|
||||
}
|
||||
// // 判断是否是扫描件pdf
|
||||
// if (PdfScanChecker.isScannedPdf(firstDocument)) {
|
||||
// // 扫描件pdf -> orc识别 -> 重组成新的文字pdf
|
||||
//
|
||||
// }
|
||||
task.setFirstPageNum(firstDocument.getNumberOfPages());
|
||||
if (firstDocument.getNumberOfPages() > 0) {
|
||||
task.setFirstContractContent(firstDocument.getPage(0).getContents().toString());
|
||||
task.setFirstWidth(firstDocument.getPage(0).getMediaBox().getWidth());
|
||||
task.setFirstHeight(firstDocument.getPage(0).getMediaBox().getHeight());
|
||||
}
|
||||
secondFile = pdfUtil.downloadFile(task.getSecondContractUrl(), task.getSecondContractName());
|
||||
PDDocument secondDocument = Loader.loadPDF(secondFile);
|
||||
if (secondDocument == null) {
|
||||
throw new ServiceException("解析PDF文件失败");
|
||||
}
|
||||
task.setSecondPageNum(secondDocument.getNumberOfPages());
|
||||
if (secondDocument.getNumberOfPages() > 0) {
|
||||
task.setSecondWidth(secondDocument.getPage(0).getMediaBox().getWidth());
|
||||
task.setSecondHeight(secondDocument.getPage(0).getMediaBox().getHeight());
|
||||
}
|
||||
firstDocument.close();
|
||||
secondDocument.close();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("文本提取失败", e);
|
||||
updateTaskStatus(task, 4, "文本提取失败");
|
||||
throw new ServiceException("文本提取失败");
|
||||
}
|
||||
updateTaskStatus(task, 2, "文本提取完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<DiffRegion> doCompare(ContractCompareTask task) {
|
||||
// TODO: 对比两张图片提取出的文本
|
||||
// String firstContractContent = task.getFirstContractContent();
|
||||
// String secondContractContent = task.getSecondContractContent();
|
||||
// // md文本按 \n 分割成行
|
||||
// List<String> firstContractLines = Arrays.asList(firstContractContent.split("\n"));
|
||||
// List<String> secondContractLines = Arrays.asList(secondContractContent.split("\n"));
|
||||
//
|
||||
// //组装结果
|
||||
// List<DiffRegion> regions = new ArrayList<>();
|
||||
// int maxLines = Math.max(firstContractLines.size(), secondContractLines.size());
|
||||
//
|
||||
// for (int i = 0; i < maxLines; i++) {
|
||||
// String firstLine = i < firstContractLines.size() ? firstContractLines.get(i) : "";
|
||||
// String secondLine = i < secondContractLines.size() ? secondContractLines.get(i) : "";
|
||||
//
|
||||
// // 行号从1开始计算
|
||||
// int srcLineNo = i + 1;
|
||||
// int dstLineNo = i + 1;
|
||||
//
|
||||
// // 如果两行内容相同,则跳过
|
||||
// if (firstLine.equals(secondLine)) {
|
||||
// continue;
|
||||
// }
|
||||
// // 生成字符级差异
|
||||
// List<DiffRow> charDiffs = DiffRowGenerator.create()
|
||||
// .showInlineDiffs(false)
|
||||
// .inlineDiffByWord(false) // 字符级而不是单词级
|
||||
// .ignoreWhiteSpaces(true)
|
||||
// .build()
|
||||
// .generateDiffRows(
|
||||
// Arrays.asList(firstLine.split("(?!^)")),
|
||||
// Arrays.asList(secondLine.split("(?!^)"))
|
||||
// );
|
||||
// // 每行的字符级差异
|
||||
// for (DiffRow charDiff : charDiffs) {
|
||||
// switch (charDiff.getTag()) {
|
||||
// case EQUAL:
|
||||
// break;
|
||||
// case DELETE:
|
||||
// regions.add(buildRegion("delete", charDiff.getOldLine(), charDiff.getNewLine(), srcLineNo, dstLineNo));
|
||||
// break;
|
||||
// case INSERT:
|
||||
// regions.add(buildRegion("insert", charDiff.getOldLine(), charDiff.getNewLine(), srcLineNo, dstLineNo));
|
||||
// break;
|
||||
// case CHANGE:
|
||||
// regions.add(buildRegion("change", charDiff.getOldLine(), charDiff.getNewLine(), srcLineNo, dstLineNo));
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
return pdfDiffWithPosition.compare(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContractCompareResult buildResult(ContractCompareTask task, DiffRegion change) {
|
||||
// // TODO: 构建图片比对结果(注意:图片无精确坐标,可能需模拟或标记区域)
|
||||
// ContractCompareResult result = new ContractCompareResult();
|
||||
// result.setTaskId(task.getTaskId());
|
||||
// // 内容
|
||||
// result.setBaseDiffContent(change.getSourceText());
|
||||
// result.setCompareDiffContent(change.getTargetText());
|
||||
// // 页码
|
||||
// result.setBasePageNum(change.getSourcePage());
|
||||
// result.setComparePageNum(change.getTargetPage());
|
||||
// // 行号
|
||||
// result.setBaseLineNum(change.getSourceLine());
|
||||
// result.setCompareLineNum(change.getTargetLine());
|
||||
// // 差异类型
|
||||
// // 这里主要以目标文本为视角
|
||||
// // 如果是 change 那么源文本是change,目标文本是change
|
||||
// // 如果是 insert 那么源文本是delete,目标文本是insert
|
||||
// // 如果是 delete 那么源文本是insert,目标文本是delete
|
||||
// if ("change".equals(change.getChangeType())) {
|
||||
// result.setBaseDiffType("change");
|
||||
// result.setCompareDiffType("change");
|
||||
// } else if ("insert".equals(change.getChangeType())) {
|
||||
// result.setBaseDiffType("delete");
|
||||
// result.setCompareDiffType("insert");
|
||||
// } else if ("delete".equals(change.getChangeType())) {
|
||||
// result.setBaseDiffType("insert");
|
||||
// result.setCompareDiffType("delete");
|
||||
// }
|
||||
// return result;
|
||||
|
||||
ContractCompareResult contractCompareResult = new ContractCompareResult();
|
||||
contractCompareResult.setTaskId(task.getTaskId());
|
||||
|
||||
contractCompareResult.setBaseDiffContent(StringUtils.isNotBlank(change.getSourceText()) ? change.getSourceText() : "");
|
||||
contractCompareResult.setCompareDiffContent(StringUtils.isNotBlank(change.getTargetText()) ? change.getTargetText() : "");
|
||||
|
||||
// 源文本的变动类型,要根据目标的变动类型来处理,change - change , delete - insert, insert - delete
|
||||
if (Objects.equals(change.getChangeType(), DeltaType.CHANGE.name())) {
|
||||
contractCompareResult.setBaseDiffType("change");
|
||||
contractCompareResult.setCompareDiffType("change");
|
||||
} else if (Objects.equals(change.getChangeType(), DeltaType.DELETE.name())) {
|
||||
contractCompareResult.setBaseDiffType("insert");
|
||||
contractCompareResult.setCompareDiffType("delete");
|
||||
} else if (Objects.equals(change.getChangeType(), DeltaType.INSERT.name())) {
|
||||
contractCompareResult.setBaseDiffType("delete");
|
||||
contractCompareResult.setCompareDiffType("insert");
|
||||
}
|
||||
|
||||
if (change.getSourceBox() != null) {
|
||||
BoundingBox sourceBox = change.getSourceBox();
|
||||
contractCompareResult.setBaseBoxArea(
|
||||
sourceBox.getX() + "," + sourceBox.getY() + "," + sourceBox.getWidth() + "," + sourceBox.getHeight());
|
||||
|
||||
contractCompareResult.setBasePageNum(change.getSourcePage());
|
||||
} else {
|
||||
// 没有源文本,则取目标文本的定位
|
||||
BoundingBox targetBox = change.getTargetBox();
|
||||
contractCompareResult.setBaseBoxArea(
|
||||
targetBox.getX() + "," + targetBox.getY() + "," + targetBox.getWidth() + "," + targetBox.getHeight());
|
||||
contractCompareResult.setBasePageNum(change.getTargetPage());
|
||||
}
|
||||
if (change.getTargetBox() != null) {
|
||||
BoundingBox targetBox = change.getTargetBox();
|
||||
contractCompareResult.setCompareBoxArea(
|
||||
targetBox.getX() + "," + targetBox.getY() + "," + targetBox.getWidth() + "," + targetBox.getHeight());
|
||||
contractCompareResult.setComparePageNum(change.getTargetPage());
|
||||
} else {
|
||||
// 没有目标文本,则取源文本的定位
|
||||
BoundingBox sourceBox = change.getSourceBox();
|
||||
contractCompareResult.setCompareBoxArea(
|
||||
sourceBox.getX() + "," + sourceBox.getY() + "," + sourceBox.getWidth() + "," + sourceBox.getHeight());
|
||||
contractCompareResult.setComparePageNum(change.getSourcePage());
|
||||
}
|
||||
contractCompareResult.setCreateTime(new Date());
|
||||
return contractCompareResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveResults(List<ContractCompareResult> results) {
|
||||
compareResultMapper.insertBatch(results);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateTaskStatus(ContractCompareTask task, int status, String msg) {
|
||||
task.setStatus(status);
|
||||
task.setErrorMsg(msg);
|
||||
contractCompareTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String fileExtension) {
|
||||
return Arrays.asList("png", "jpg", "jpeg").contains(fileExtension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Markdown 字符串写入指定文件
|
||||
*
|
||||
* @param content Markdown 内容
|
||||
* @param fileName 文件名
|
||||
* @return 文件相对路径
|
||||
*/
|
||||
public String writeMarkdown(String content, String fileName) {
|
||||
|
||||
String needUploadName = DateUtils.datePath() + "/" + IdUtil.fastUUID() + "/" +
|
||||
fileName.substring(0, fileName.lastIndexOf(".")) + ".md";
|
||||
String filePath = QingYunConfig.getUploadPath() + "/" + needUploadName;
|
||||
Path targetPath = Paths.get(filePath);
|
||||
// 确保父目录存在
|
||||
try {
|
||||
if (targetPath.getParent() != null) {
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("无法创建目录:" + targetPath, e);
|
||||
}
|
||||
|
||||
// 使用 try-with-resources 自动关闭流,指定 UTF-8 编码避免中文乱码
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(targetPath, StandardCharsets.UTF_8)) {
|
||||
writer.write(content);
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("写入 Markdown 文件失败:" + targetPath, e);
|
||||
}
|
||||
return "/profile/upload/" + needUploadName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将md文本内容写入 PDF 文件
|
||||
*
|
||||
* @param content 文本内容
|
||||
* @param fileName 文件名
|
||||
* @return 文件相对路径
|
||||
*/
|
||||
public String writePdf(String content, String fileName) {
|
||||
String needUploadName = DateUtils.datePath() + "/" + IdUtil.fastUUID() + "/" +
|
||||
fileName.substring(0, fileName.lastIndexOf(".")) + ".pdf";
|
||||
String filePath = QingYunConfig.getUploadPath() + "/" + needUploadName;
|
||||
Path targetPath = Paths.get(filePath);
|
||||
|
||||
// 确保父目录存在
|
||||
try {
|
||||
if (targetPath.getParent() != null) {
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("无法创建目录:" + targetPath, e);
|
||||
}
|
||||
|
||||
// 使用 PDFBox 将文本内容写入 PDF 文件
|
||||
try {
|
||||
PDDocument document = new PDDocument();
|
||||
PDPage page = new PDPage();
|
||||
document.addPage(page);
|
||||
|
||||
PDPageContentStream contentStream = new PDPageContentStream(document, page);
|
||||
|
||||
InputStream fontStream = Thread.currentThread().getContextClassLoader()
|
||||
.getResourceAsStream("fonts/SimSun.ttf");
|
||||
// 加载默认字体以支持中文
|
||||
PDFont font = PDType0Font.load(document, fontStream);
|
||||
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(font, 10);
|
||||
contentStream.newLineAtOffset(20, 750);
|
||||
|
||||
// 将文本按行分割并写入 PDF
|
||||
String[] lines = content.split("\n");
|
||||
for (String line : lines) {
|
||||
contentStream.showText(line);
|
||||
contentStream.newLineAtOffset(0, -12); // 每行间隔15个单位
|
||||
}
|
||||
|
||||
contentStream.endText();
|
||||
contentStream.close();
|
||||
|
||||
// 保存文档
|
||||
document.save(targetPath.toFile());
|
||||
document.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("写入 PDF 文件失败:" + targetPath, e);
|
||||
}
|
||||
return "/profile/upload/" + needUploadName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Markdown 转换为带样式的 HTML
|
||||
*/
|
||||
private String mdToHtml(String markdown) {
|
||||
com.vladsch.flexmark.util.data.MutableDataSet options = new com.vladsch.flexmark.util.data.MutableDataSet();
|
||||
Parser parser = Parser.builder(options).build();
|
||||
|
||||
HtmlRenderer renderer = HtmlRenderer.builder(options).build();
|
||||
|
||||
Node document = parser.parse(markdown);
|
||||
return renderer.render(document); // 输出 HTML 字符串
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 HTML 渲染为 PDF 并保存到指定文件
|
||||
*/
|
||||
private void htmlToPdf(String html, File outputFile) throws Exception {
|
||||
try (OutputStream os = Files.newOutputStream(outputFile.toPath())) {
|
||||
ITextRenderer renderer = new ITextRenderer();
|
||||
|
||||
// 1. 注册中文字体(关键!解决中文乱码)
|
||||
// 假设 SimSun.ttf 放在 resources/fonts 目录下(需提前放入)
|
||||
InputStream fontStream = getClass().getClassLoader()
|
||||
.getResourceAsStream("fonts/SimSun.ttf");
|
||||
if (fontStream == null) {
|
||||
throw new RuntimeException("未找到中文字体文件:fonts/SimSun.ttf");
|
||||
}
|
||||
|
||||
ITextFontResolver fontResolver = renderer.getFontResolver();
|
||||
// 注册宋体(支持中文,BaseFont.IDENTITY_H 表示 Unicode 编码)
|
||||
fontResolver.addFont("fonts/SimSun.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
|
||||
|
||||
// 2. 加载 CSS 样式(控制 PDF 外观)
|
||||
String css = "@page { size: A4; margin: 2cm; } " +
|
||||
"body { font-family: 'SimSun', serif; line-height: 1.8; font-size: 12pt; } " +
|
||||
"h1 { color: #2c3e50; font-size: 18pt; border-bottom: 2px solid #34495e; padding-bottom: 0.3em; } " +
|
||||
"h2 { color: #34495e; font-size: 16pt; margin: 1em 0 0.8em; } " +
|
||||
"p { margin: 0.8em 0; text-align: justify; } " +
|
||||
"pre { background: #f8f8f8; padding: 1em; border-radius: 4px; overflow-x: auto; } " +
|
||||
"code { font-family: 'Consolas', monospace; font-size: 10pt; } " +
|
||||
"ul, ol { margin: 0.8em 0; padding-left: 2em; } " +
|
||||
"li { margin: 0.3em 0; } " +
|
||||
"a { color: #2980b9; text-decoration: none; } " +
|
||||
"a:hover { text-decoration: underline; }";
|
||||
|
||||
// 3. 渲染 HTML 到 PDF
|
||||
renderer.setDocumentFromString(html); // 关联 HTML 和 CSS
|
||||
renderer.layout(); // 计算布局
|
||||
renderer.createPDF(os); // 输出 PDF 到流
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 DiffRegion
|
||||
*/
|
||||
private DiffRegion buildRegion(String type,
|
||||
String srcText, String dstText,
|
||||
Integer srcLine, Integer dstLine) {
|
||||
DiffRegion r = new DiffRegion();
|
||||
r.setChangeType(type);
|
||||
r.setSourceText(srcText);
|
||||
r.setTargetText(dstText);
|
||||
r.setSourceLine(srcLine);
|
||||
r.setTargetLine(dstLine);
|
||||
r.setSourcePage(1);
|
||||
r.setTargetPage(1);
|
||||
r.setSourceBox(null);
|
||||
r.setTargetBox(null);
|
||||
return r;
|
||||
}
|
||||
|
||||
public String removeMarkdownSyntax(String markdown) {
|
||||
// 1. 保留换行符(\n)作为分隔符
|
||||
String[] lines = markdown.split("\n", -1);
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (String line : lines) {
|
||||
// 2. 移除各种Markdown语法
|
||||
String cleanLine = line
|
||||
// 移除标题标记 (## 标题 → 标题)
|
||||
.replaceAll("^#{1,6}\\s*", "")
|
||||
// 移除列表标记 (* item, - item, 1. item)
|
||||
.replaceAll("^\\s*([-*+]|[0-9]+\\.)\\s+", "")
|
||||
// 移除引用标记 (> 引用)
|
||||
.replaceAll("^>+\\s*", "")
|
||||
// 移除代码块标记 (``` 和 `)
|
||||
.replaceAll("`{3}.*", "") // 代码块开始标记
|
||||
.replaceAll("`", "") // 行内代码标记
|
||||
// 移除粗体和斜体标记 (**bold** → bold)
|
||||
.replaceAll("\\*{1,2}(.*?)\\*{1,2}", "$1")
|
||||
.replaceAll("_{1,2}(.*?)_{1,2}", "$1")
|
||||
// 移除链接和图片标记 ([text](url) → text
|
||||
.replaceAll("!?\\[([^\\]]*)\\]\\([^\\)]+\\)", "$1")
|
||||
// 移除表格标记 (| header | → header)
|
||||
.replaceAll("\\|\\s*:?[-]+\\s*", "") // 表头分隔行
|
||||
.replaceAll("\\|", " ") // 表格分隔符
|
||||
// 移除HTML标签(可选)
|
||||
.replaceAll("<[^>]+>", "")
|
||||
// 移除水平线 (---)
|
||||
.replaceAll("^[-*]{3,}\\s*$", "")
|
||||
// 移除任务列表标记 ([x])
|
||||
.replaceAll("\\[x\\]|\\[\\s\\]", "")
|
||||
// 移除尾随空格
|
||||
.trim();
|
||||
|
||||
result.append(cleanLine).append("\n");
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.qingyun.service.compare.strategy;
|
||||
|
||||
import com.github.difflib.patch.DeltaType;
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import com.qingyun.common.utils.StringUtils;
|
||||
import com.qingyun.service.compare.BoundingBox;
|
||||
import com.qingyun.service.compare.DiffRegion;
|
||||
import com.qingyun.service.compare.PdfDiffWithPosition;
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import com.qingyun.service.mapper.ContractCompareResultMapper;
|
||||
import com.qingyun.service.mapper.ContractCompareTaskMapper;
|
||||
import com.qingyun.service.utils.PdfUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
@Component
|
||||
@Order(1)
|
||||
public class PdfComparisonStrategy extends AbstractFileComparisonStrategy implements Ordered {
|
||||
|
||||
@Autowired
|
||||
private PdfUtil pdfUtil;
|
||||
|
||||
@Autowired
|
||||
private PdfDiffWithPosition pdfDiffWithPosition;
|
||||
|
||||
@Autowired
|
||||
private ContractCompareResultMapper compareResultMapper;
|
||||
|
||||
@Autowired
|
||||
private ContractCompareTaskMapper contractCompareTaskMapper;
|
||||
|
||||
@Override
|
||||
protected void doExtract(ContractCompareTask task) {
|
||||
if (!task.getFirstContractUrl().endsWith(".pdf")) {
|
||||
task.setFirstContractName(task.getFirstContractName() + ".pdf");
|
||||
}
|
||||
if (!task.getSecondContractUrl().endsWith(".pdf")) {
|
||||
task.setSecondContractName(task.getSecondContractName() + ".pdf");
|
||||
}
|
||||
|
||||
File firstFile = null;
|
||||
File secondFile = null;
|
||||
try {
|
||||
firstFile = pdfUtil.downloadFile(task.getFirstContractUrl(), task.getFirstContractName());
|
||||
PDDocument firstDocument = Loader.loadPDF(firstFile);
|
||||
if (firstDocument == null) {
|
||||
throw new ServiceException("解析PDF文件失败");
|
||||
}
|
||||
// // 判断是否是扫描件pdf
|
||||
// if (PdfScanChecker.isScannedPdf(firstDocument)) {
|
||||
// // 扫描件pdf -> orc识别 -> 重组成新的文字pdf
|
||||
//
|
||||
// }
|
||||
task.setFirstPageNum(firstDocument.getNumberOfPages());
|
||||
if (firstDocument.getNumberOfPages() > 0) {
|
||||
task.setFirstContractContent(firstDocument.getPage(0).getContents().toString());
|
||||
task.setFirstWidth(firstDocument.getPage(0).getMediaBox().getWidth());
|
||||
task.setFirstHeight(firstDocument.getPage(0).getMediaBox().getHeight());
|
||||
}
|
||||
secondFile = pdfUtil.downloadFile(task.getSecondContractUrl(), task.getSecondContractName());
|
||||
PDDocument secondDocument = Loader.loadPDF(secondFile);
|
||||
if (secondDocument == null) {
|
||||
throw new ServiceException("解析PDF文件失败");
|
||||
}
|
||||
task.setSecondPageNum(secondDocument.getNumberOfPages());
|
||||
if (secondDocument.getNumberOfPages() > 0) {
|
||||
task.setSecondWidth(secondDocument.getPage(0).getMediaBox().getWidth());
|
||||
task.setSecondHeight(secondDocument.getPage(0).getMediaBox().getHeight());
|
||||
}
|
||||
firstDocument.close();
|
||||
secondDocument.close();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析PDF文件失败", e);
|
||||
updateTaskStatus(task, 4, "解析PDF文件失败");
|
||||
return;
|
||||
}
|
||||
updateTaskStatus(task, 2, "文本提取完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<DiffRegion> doCompare(ContractCompareTask task) {
|
||||
return pdfDiffWithPosition.compare(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContractCompareResult buildResult(ContractCompareTask task, DiffRegion change) {
|
||||
ContractCompareResult contractCompareResult = new ContractCompareResult();
|
||||
contractCompareResult.setTaskId(task.getTaskId());
|
||||
|
||||
contractCompareResult.setBaseDiffContent(StringUtils.isNotBlank(change.getSourceText()) ? change.getSourceText() : "");
|
||||
contractCompareResult.setCompareDiffContent(StringUtils.isNotBlank(change.getTargetText()) ? change.getTargetText() : "");
|
||||
|
||||
// 源文本的变动类型,要根据目标的变动类型来处理,change - change , delete - insert, insert - delete
|
||||
if (Objects.equals(change.getChangeType(), DeltaType.CHANGE.name())) {
|
||||
contractCompareResult.setBaseDiffType("change");
|
||||
contractCompareResult.setCompareDiffType("change");
|
||||
} else if (Objects.equals(change.getChangeType(), DeltaType.DELETE.name())) {
|
||||
contractCompareResult.setBaseDiffType("insert");
|
||||
contractCompareResult.setCompareDiffType("delete");
|
||||
} else if (Objects.equals(change.getChangeType(), DeltaType.INSERT.name())) {
|
||||
contractCompareResult.setBaseDiffType("delete");
|
||||
contractCompareResult.setCompareDiffType("insert");
|
||||
}
|
||||
|
||||
if (change.getSourceBox() != null) {
|
||||
BoundingBox sourceBox = change.getSourceBox();
|
||||
contractCompareResult.setBaseBoxArea(
|
||||
sourceBox.getX() + "," + sourceBox.getY() + "," + sourceBox.getWidth() + "," + sourceBox.getHeight());
|
||||
|
||||
contractCompareResult.setBasePageNum(change.getSourcePage());
|
||||
} else {
|
||||
// 没有源文本,则取目标文本的定位
|
||||
BoundingBox targetBox = change.getTargetBox();
|
||||
contractCompareResult.setBaseBoxArea(
|
||||
targetBox.getX() + "," + targetBox.getY() + "," + targetBox.getWidth() + "," + targetBox.getHeight());
|
||||
contractCompareResult.setBasePageNum(change.getTargetPage());
|
||||
}
|
||||
if (change.getTargetBox() != null) {
|
||||
BoundingBox targetBox = change.getTargetBox();
|
||||
contractCompareResult.setCompareBoxArea(
|
||||
targetBox.getX() + "," + targetBox.getY() + "," + targetBox.getWidth() + "," + targetBox.getHeight());
|
||||
contractCompareResult.setComparePageNum(change.getTargetPage());
|
||||
} else {
|
||||
// 没有目标文本,则取源文本的定位
|
||||
BoundingBox sourceBox = change.getSourceBox();
|
||||
contractCompareResult.setCompareBoxArea(
|
||||
sourceBox.getX() + "," + sourceBox.getY() + "," + sourceBox.getWidth() + "," + sourceBox.getHeight());
|
||||
contractCompareResult.setComparePageNum(change.getSourcePage());
|
||||
}
|
||||
contractCompareResult.setCreateTime(new Date());
|
||||
return contractCompareResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveResults(List<ContractCompareResult> results) {
|
||||
compareResultMapper.insertBatch(results);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateTaskStatus(ContractCompareTask task, int status, String msg) {
|
||||
task.setStatus(status);
|
||||
task.setErrorMsg(msg);
|
||||
contractCompareTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String fileExtension) {
|
||||
return "pdf".equals(fileExtension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package com.qingyun.service.compare.strategy;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.github.difflib.patch.DeltaType;
|
||||
import com.qingyun.common.config.QingYunConfig;
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import com.qingyun.common.utils.DateUtils;
|
||||
import com.qingyun.common.utils.StringUtils;
|
||||
import com.qingyun.service.compare.BoundingBox;
|
||||
import com.qingyun.service.compare.DiffRegion;
|
||||
import com.qingyun.service.compare.PdfDiffWithPosition;
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import com.qingyun.service.mapper.ContractCompareResultMapper;
|
||||
import com.qingyun.service.mapper.ContractCompareTaskMapper;
|
||||
import com.qingyun.service.utils.PdfUtil;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
@Order(3)
|
||||
public class WordComparisonStrategy extends AbstractFileComparisonStrategy implements Ordered {
|
||||
|
||||
@Autowired
|
||||
private PdfUtil pdfUtil;
|
||||
|
||||
@Autowired
|
||||
private PdfDiffWithPosition pdfDiffWithPosition;
|
||||
|
||||
@Autowired
|
||||
private ContractCompareResultMapper compareResultMapper;
|
||||
|
||||
@Autowired
|
||||
private ContractCompareTaskMapper contractCompareTaskMapper;
|
||||
|
||||
@Override
|
||||
protected void doExtract(ContractCompareTask task) {
|
||||
|
||||
|
||||
File firstFile = null;
|
||||
File secondFile = null;
|
||||
try {
|
||||
// 转换成pdf
|
||||
Pair<String, File> firstMap = processContractFile(task.getFirstContractUrl(), task.getFirstContractName());
|
||||
Pair<String, File> secondMap = processContractFile(task.getSecondContractUrl(), task.getSecondContractName());
|
||||
|
||||
task.setFirstContractUrl(firstMap.getLeft());
|
||||
task.setSecondContractUrl(secondMap.getLeft());
|
||||
|
||||
task.setFirstContractName(task.getFirstContractName().replaceAll("\\.(doc|docx)$", ".pdf"));
|
||||
task.setSecondContractName(task.getSecondContractName().replaceAll("\\.(doc|docx)$", ".pdf"));
|
||||
|
||||
firstFile = firstMap.getRight();
|
||||
PDDocument firstDocument = Loader.loadPDF(firstFile);
|
||||
if (firstDocument == null) {
|
||||
throw new ServiceException("解析PDF文件失败");
|
||||
}
|
||||
// // 判断是否是扫描件pdf
|
||||
// if (PdfScanChecker.isScannedPdf(firstDocument)) {
|
||||
// // 扫描件pdf -> orc识别 -> 重组成新的文字pdf
|
||||
//
|
||||
// }
|
||||
task.setFirstPageNum(firstDocument.getNumberOfPages());
|
||||
if (firstDocument.getNumberOfPages() > 0) {
|
||||
task.setFirstContractContent(firstDocument.getPage(0).getContents().toString());
|
||||
task.setFirstWidth(firstDocument.getPage(0).getMediaBox().getWidth());
|
||||
task.setFirstHeight(firstDocument.getPage(0).getMediaBox().getHeight());
|
||||
}
|
||||
secondFile = secondMap.getRight();
|
||||
PDDocument secondDocument = Loader.loadPDF(secondFile);
|
||||
if (secondDocument == null) {
|
||||
throw new ServiceException("解析PDF文件失败");
|
||||
}
|
||||
task.setSecondPageNum(secondDocument.getNumberOfPages());
|
||||
if (secondDocument.getNumberOfPages() > 0) {
|
||||
task.setSecondWidth(secondDocument.getPage(0).getMediaBox().getWidth());
|
||||
task.setSecondHeight(secondDocument.getPage(0).getMediaBox().getHeight());
|
||||
}
|
||||
firstDocument.close();
|
||||
secondDocument.close();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析PDF文件失败", e);
|
||||
updateTaskStatus(task, 4, "解析PDF文件失败");
|
||||
return;
|
||||
}
|
||||
updateTaskStatus(task, 2, "文本提取完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<DiffRegion> doCompare(ContractCompareTask task) {
|
||||
return pdfDiffWithPosition.compare(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContractCompareResult buildResult(ContractCompareTask task, DiffRegion change) {
|
||||
ContractCompareResult contractCompareResult = new ContractCompareResult();
|
||||
contractCompareResult.setTaskId(task.getTaskId());
|
||||
|
||||
contractCompareResult.setBaseDiffContent(StringUtils.isNotBlank(change.getSourceText()) ? change.getSourceText() : "");
|
||||
contractCompareResult.setCompareDiffContent(StringUtils.isNotBlank(change.getTargetText()) ? change.getTargetText() : "");
|
||||
|
||||
// 源文本的变动类型,要根据目标的变动类型来处理,change - change , delete - insert, insert - delete
|
||||
if (Objects.equals(change.getChangeType(), DeltaType.CHANGE.name())) {
|
||||
contractCompareResult.setBaseDiffType("change");
|
||||
contractCompareResult.setCompareDiffType("change");
|
||||
} else if (Objects.equals(change.getChangeType(), DeltaType.DELETE.name())) {
|
||||
contractCompareResult.setBaseDiffType("insert");
|
||||
contractCompareResult.setCompareDiffType("delete");
|
||||
} else if (Objects.equals(change.getChangeType(), DeltaType.INSERT.name())) {
|
||||
contractCompareResult.setBaseDiffType("delete");
|
||||
contractCompareResult.setCompareDiffType("insert");
|
||||
}
|
||||
|
||||
if (change.getSourceBox() != null) {
|
||||
BoundingBox sourceBox = change.getSourceBox();
|
||||
contractCompareResult.setBaseBoxArea(
|
||||
sourceBox.getX() + "," + sourceBox.getY() + "," + sourceBox.getWidth() + "," + sourceBox.getHeight());
|
||||
|
||||
contractCompareResult.setBasePageNum(change.getSourcePage());
|
||||
} else {
|
||||
// 没有源文本,则取目标文本的定位
|
||||
BoundingBox targetBox = change.getTargetBox();
|
||||
contractCompareResult.setBaseBoxArea(
|
||||
targetBox.getX() + "," + targetBox.getY() + "," + targetBox.getWidth() + "," + targetBox.getHeight());
|
||||
contractCompareResult.setBasePageNum(change.getTargetPage());
|
||||
}
|
||||
if (change.getTargetBox() != null) {
|
||||
BoundingBox targetBox = change.getTargetBox();
|
||||
contractCompareResult.setCompareBoxArea(
|
||||
targetBox.getX() + "," + targetBox.getY() + "," + targetBox.getWidth() + "," + targetBox.getHeight());
|
||||
contractCompareResult.setComparePageNum(change.getTargetPage());
|
||||
} else {
|
||||
// 没有目标文本,则取源文本的定位
|
||||
BoundingBox sourceBox = change.getSourceBox();
|
||||
contractCompareResult.setCompareBoxArea(
|
||||
sourceBox.getX() + "," + sourceBox.getY() + "," + sourceBox.getWidth() + "," + sourceBox.getHeight());
|
||||
contractCompareResult.setComparePageNum(change.getSourcePage());
|
||||
}
|
||||
contractCompareResult.setCreateTime(new Date());
|
||||
return contractCompareResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveResults(List<ContractCompareResult> results) {
|
||||
compareResultMapper.insertBatch(results);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateTaskStatus(ContractCompareTask task, int status, String msg) {
|
||||
task.setStatus(status);
|
||||
task.setErrorMsg(msg);
|
||||
contractCompareTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String fileExtension) {
|
||||
return "doc".equals(fileExtension) || "docx".equals(fileExtension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上传的合同文件,如果上传的是Word文件则转换为PDF,并更新合同URL
|
||||
*
|
||||
* @return 处理后的PDF文件
|
||||
* @throws IOException IO异常
|
||||
* @throws ServiceException 服务异常
|
||||
*/
|
||||
private Pair<String, File> processContractFile(String url, String fileName) throws IOException, ServiceException {
|
||||
File pdfFile;
|
||||
File tempFile = null;
|
||||
try {
|
||||
// 临时文件
|
||||
tempFile = pdfUtil.downloadFile(url, fileName);
|
||||
fileName = fileName.toLowerCase();
|
||||
|
||||
// 判断文件类型
|
||||
// 如果是word类型转换成pdf
|
||||
pdfFile = pdfUtil.convertWordToPdf(tempFile);
|
||||
// 在用户上传的word文件路径目录下创建新的pdf格式文件路径
|
||||
String needUploadName = DateUtils.datePath() + "/" + IdUtil.fastUUID() + "/" +
|
||||
fileName.substring(0, fileName.lastIndexOf(".")) + ".pdf";
|
||||
String filePath = QingYunConfig.getUploadPath() + "/" + needUploadName;
|
||||
|
||||
// 确保目录存在
|
||||
File destFile = new File(filePath);
|
||||
destFile.getParentFile().mkdirs();
|
||||
|
||||
// 将转换后的PDF文件移动到目标位置
|
||||
File targetFile = new File(filePath);
|
||||
if (pdfFile.renameTo(targetFile)) {
|
||||
pdfFile = targetFile;
|
||||
} else {
|
||||
// 如果renameTo失败,尝试复制文件
|
||||
try (FileInputStream fis = new FileInputStream(pdfFile)) {
|
||||
Files.copy(pdfFile.toPath(), targetFile.toPath());
|
||||
pdfFile = targetFile;
|
||||
}
|
||||
}
|
||||
// 更新合同URL为新的PDF文件路径
|
||||
String newUrl = "/profile/upload/" + needUploadName;
|
||||
|
||||
return Pair.of(newUrl, pdfFile);
|
||||
} catch (Exception e) {
|
||||
// 清理临时文件
|
||||
if (tempFile != null && tempFile.exists()) {
|
||||
Files.deleteIfExists(tempFile.toPath());
|
||||
}
|
||||
throw new ServiceException("处理文件失败: " + e.getMessage());
|
||||
} finally {
|
||||
if (tempFile != null) {
|
||||
Files.deleteIfExists(tempFile.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.qingyun.service.domain;
|
||||
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* 对比结果对象 contract_compare_result
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@TableName("contract_compare_result")
|
||||
public class ContractCompareResult {
|
||||
|
||||
|
||||
/**
|
||||
* 结果ID
|
||||
*/
|
||||
@TableId(value = "result_id")
|
||||
private Long resultId;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 源文件定位
|
||||
*/
|
||||
private String baseBoxArea;
|
||||
/**
|
||||
* 源文件差异内容
|
||||
*/
|
||||
private String baseDiffContent;
|
||||
/**
|
||||
* 源文件差异类型(insert,update, delete)
|
||||
*/
|
||||
private String baseDiffType;
|
||||
/**
|
||||
* 源文件页码
|
||||
*/
|
||||
private Integer basePageNum;
|
||||
|
||||
/**
|
||||
* 源文件行号
|
||||
*/
|
||||
private Integer baseLineNum;
|
||||
|
||||
/**
|
||||
* 对比文件定位
|
||||
*/
|
||||
private String compareBoxArea;
|
||||
/**
|
||||
* 对比文件差异内容
|
||||
*/
|
||||
private String compareDiffContent;
|
||||
/**
|
||||
* 对比文件差异类型(insert,update, delete)
|
||||
*/
|
||||
private String compareDiffType;
|
||||
/**
|
||||
* 对比文件页码
|
||||
*/
|
||||
private Integer comparePageNum;
|
||||
|
||||
/**
|
||||
* 对比文件行号
|
||||
*/
|
||||
private Integer compareLineNum;
|
||||
|
||||
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.qingyun.service.domain;
|
||||
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* 对比任务对象 contract_compare_task
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@TableName("contract_compare_task")
|
||||
public class ContractCompareTask implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@TableId(value = "task_id")
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 任务文件类型 pdf md 纯文本 txt ...
|
||||
*/
|
||||
private String taskType;
|
||||
|
||||
/**
|
||||
* 对比规则
|
||||
*/
|
||||
private String compareRule;
|
||||
|
||||
/**
|
||||
* 第一份合同地址
|
||||
*/
|
||||
private String firstContractUrl;
|
||||
|
||||
|
||||
/**
|
||||
* 第一份合同名称
|
||||
*/
|
||||
private String firstContractName;
|
||||
|
||||
/**
|
||||
* 第一份合同内容
|
||||
*/
|
||||
private String firstContractContent;
|
||||
|
||||
/**
|
||||
* 第一份合同页数
|
||||
*/
|
||||
private Integer firstPageNum;
|
||||
|
||||
/**
|
||||
* 第一份合同 宽度 pt
|
||||
*/
|
||||
private Float firstWidth;
|
||||
|
||||
/**
|
||||
* 第一份合同 高度 pt
|
||||
*/
|
||||
private Float firstHeight;
|
||||
|
||||
/**
|
||||
* 第二份合同地址
|
||||
*/
|
||||
private String secondContractUrl;
|
||||
|
||||
/**
|
||||
* 第二份合同内容
|
||||
*/
|
||||
private String secondContractContent;
|
||||
|
||||
/**
|
||||
* 第二份合同名称
|
||||
*/
|
||||
private String secondContractName;
|
||||
|
||||
/**
|
||||
* 第二份合同 宽度 pt
|
||||
*/
|
||||
private Float secondWidth;
|
||||
|
||||
/**
|
||||
* 第二份合同 高度 pt
|
||||
*/
|
||||
private Float secondHeight;
|
||||
|
||||
/**
|
||||
* 第二份合同页数
|
||||
*/
|
||||
private Integer secondPageNum;
|
||||
|
||||
/**
|
||||
* 状态(0:未开始 1:获取原文中 2:对比中 3:已完成 4:失败)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 错误 信息
|
||||
*/
|
||||
private String errorMsg;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 逻辑删除
|
||||
*/
|
||||
@TableLogic
|
||||
private Long delFlag;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.qingyun.service.domain;
|
||||
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* 模型配置对象 contract_model_config
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@TableName("contract_model_config")
|
||||
public class ContractModelConfig implements Serializable {
|
||||
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 配置ID
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 模型厂商
|
||||
*/
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 模型版本
|
||||
*/
|
||||
private String modelVersion;
|
||||
|
||||
/**
|
||||
* 模型地址
|
||||
*/
|
||||
private String modelUrl;
|
||||
|
||||
/**
|
||||
* 模型密钥
|
||||
*/
|
||||
private String modelKey;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.qingyun.service.domain;
|
||||
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/**
|
||||
* 审查结果对象 contract_review_result
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@TableName("contract_review_result")
|
||||
public class ContractReviewResult implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 结果ID
|
||||
*/
|
||||
@TableId(value = "result_id")
|
||||
private Long resultId;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 合同原文
|
||||
*/
|
||||
private String contractContent;
|
||||
|
||||
/**
|
||||
* 风险提示
|
||||
*/
|
||||
private String riskTips;
|
||||
|
||||
/**
|
||||
* 风险等级
|
||||
*/
|
||||
private String riskLevel;
|
||||
|
||||
/**
|
||||
* 审查依据
|
||||
*/
|
||||
private String reviewBasis;
|
||||
|
||||
/**
|
||||
* 修改示例
|
||||
*/
|
||||
private String modifyExample;
|
||||
|
||||
/**
|
||||
* 规格类型(1:规则 2:要素)
|
||||
*/
|
||||
private Integer specType;
|
||||
|
||||
/**
|
||||
* 对应的合同页码
|
||||
*/
|
||||
private Integer pageNum;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.qingyun.service.domain;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审查任务对象 contract_review_task
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-15
|
||||
*/
|
||||
@Data
|
||||
@TableName("contract_review_task")
|
||||
public class ContractReviewTask implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@TableId(value = "task_id")
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 规则要素模板ID
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 模型ID
|
||||
*/
|
||||
private Long modelId;
|
||||
|
||||
/**
|
||||
* 审查规则
|
||||
*/
|
||||
private String reviewRule;
|
||||
|
||||
/**
|
||||
* 审查要素
|
||||
*/
|
||||
private String reviewElement;
|
||||
|
||||
/**
|
||||
* 合同名称
|
||||
*/
|
||||
private String contractName;
|
||||
|
||||
/**
|
||||
* 合同地址
|
||||
*/
|
||||
private String contractUrl;
|
||||
|
||||
/**
|
||||
* 合同 宽度 pt
|
||||
*/
|
||||
private Float width;
|
||||
|
||||
/**
|
||||
* 合同 高度 pt
|
||||
*/
|
||||
private Float height;
|
||||
|
||||
/**
|
||||
* 合同原文MD
|
||||
*/
|
||||
private String contractContentMd;
|
||||
|
||||
/**
|
||||
* 合同图片
|
||||
*/
|
||||
private String contractPic;
|
||||
|
||||
/**
|
||||
* 页数
|
||||
*/
|
||||
private Integer pageNum;
|
||||
|
||||
/**
|
||||
* 状态(0:未开始 1:获取原文中 2:审查中 3:已审查 4:失败)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 错误 信息
|
||||
*/
|
||||
private String errorMsg;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 逻辑删除
|
||||
*/
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.qingyun.service.domain;
|
||||
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* 规则要素模板对象 contract_rule_template
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@TableName("contract_rule_template")
|
||||
public class ContractRuleTemplate implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模板名称
|
||||
*/
|
||||
private String templateName;
|
||||
|
||||
/**
|
||||
* 审查规则JSON
|
||||
*/
|
||||
private String reviewRuleJson;
|
||||
|
||||
/**
|
||||
* 审查要素JSON
|
||||
*/
|
||||
private String reviewElementJson;
|
||||
|
||||
/**
|
||||
* 原始文件地址
|
||||
*/
|
||||
private String originalFileUrl;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 逻辑删除(0:正常 1:删除)
|
||||
*/
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
/**
|
||||
* 状态 0 启用 1 停用
|
||||
*/
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.qingyun.service.domain.bo;
|
||||
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.qingyun.common.core.validate.AddGroup;
|
||||
import com.qingyun.common.core.validate.EditGroup;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 对比结果业务对象 contract_compare_result
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ContractCompareResult.class, reverseConvertGenerate = false)
|
||||
public class ContractCompareResultBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 结果ID
|
||||
*/
|
||||
@NotNull(message = "结果ID不能为空", groups = { EditGroup.class })
|
||||
private Long resultId;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@NotNull(message = "任务ID不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 源文件定位
|
||||
*/
|
||||
private String baseBoxArea;
|
||||
/**
|
||||
* 源文件差异内容
|
||||
*/
|
||||
private String baseDiffContent;
|
||||
/**
|
||||
* 源文件差异类型(insert,update, delete)
|
||||
*/
|
||||
private String baseDiffType;
|
||||
/**
|
||||
* 源文件页码
|
||||
*/
|
||||
private Integer basePageNum;
|
||||
/**
|
||||
* 对比文件定位
|
||||
*/
|
||||
private String compareBoxArea;
|
||||
/**
|
||||
* 对比文件差异内容
|
||||
*/
|
||||
private String compareDiffContent;
|
||||
/**
|
||||
* 对比文件差异类型(insert,update, delete)
|
||||
*/
|
||||
private String compareDiffType;
|
||||
/**
|
||||
* 对比文件页码
|
||||
*/
|
||||
private Integer comparePageNum;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.qingyun.service.domain.bo;
|
||||
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.qingyun.common.core.validate.AddGroup;
|
||||
import com.qingyun.common.core.validate.EditGroup;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 对比任务业务对象 contract_compare_task
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = ContractCompareTask.class, reverseConvertGenerate = false)
|
||||
public class ContractCompareTaskBo {
|
||||
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@NotNull(message = "任务ID不能为空", groups = { EditGroup.class })
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 对比规则
|
||||
*/
|
||||
//@NotBlank(message = "对比规则不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String compareRule;
|
||||
|
||||
/**
|
||||
* 第一份文件地址
|
||||
*/
|
||||
@NotBlank(message = "第一份合同地址不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String firstContractUrl;
|
||||
|
||||
/**
|
||||
* 第一份合同名称
|
||||
*/
|
||||
@NotBlank(message = "第一份合同名称不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String firstContractName;
|
||||
|
||||
/**
|
||||
* 第二份合同地址
|
||||
*/
|
||||
@NotBlank(message = "第二份合同地址不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String secondContractUrl;
|
||||
|
||||
/**
|
||||
* 第二份合同名称
|
||||
*/
|
||||
@NotBlank(message = "第二份合同名称不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String secondContractName;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.qingyun.service.domain.bo;
|
||||
|
||||
import com.qingyun.service.domain.ContractModelConfig;
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.qingyun.common.core.validate.AddGroup;
|
||||
import com.qingyun.common.core.validate.EditGroup;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 模型配置业务对象 contract_model_config
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = ContractModelConfig.class, reverseConvertGenerate = false)
|
||||
public class ContractModelConfigBo {
|
||||
|
||||
/**
|
||||
* 配置ID
|
||||
*/
|
||||
@NotNull(message = "配置ID不能为空", groups = { EditGroup.class })
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@NotBlank(message = "模型名称不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 模型类型:openai deepseek 先固定定这两个选择
|
||||
*/
|
||||
@NotBlank(message = "模型类型不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 模型地址
|
||||
*/
|
||||
@NotBlank(message = "模型地址不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String modelUrl;
|
||||
|
||||
/**
|
||||
* 模型密钥 可为空
|
||||
*/
|
||||
private String modelKey;
|
||||
|
||||
/**
|
||||
* 模型版本
|
||||
*/
|
||||
private String modelVersion;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.qingyun.service.domain.bo;
|
||||
|
||||
import com.qingyun.service.domain.ContractReviewResult;
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.qingyun.common.core.validate.AddGroup;
|
||||
import com.qingyun.common.core.validate.EditGroup;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 审查结果业务对象 contract_review_result
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ContractReviewResult.class, reverseConvertGenerate = false)
|
||||
public class ContractReviewResultBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 结果ID
|
||||
*/
|
||||
@NotNull(message = "结果ID不能为空", groups = { EditGroup.class })
|
||||
private Long resultId;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@NotNull(message = "任务ID不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long taskId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.qingyun.service.domain.bo;
|
||||
|
||||
import com.qingyun.service.domain.ContractReviewTask;
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.qingyun.common.core.validate.AddGroup;
|
||||
import com.qingyun.common.core.validate.EditGroup;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 审查任务业务对象 contract_review_task
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-15
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = ContractReviewTask.class, reverseConvertGenerate = false)
|
||||
public class ContractReviewTaskBo {
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@NotNull(message = "任务ID不能为空", groups = { EditGroup.class })
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 规则要素模板ID
|
||||
*/
|
||||
@NotNull(message = "规则要素模板ID不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 模型ID
|
||||
*/
|
||||
@NotNull(message = "模型ID不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long modelId;
|
||||
|
||||
/**
|
||||
* 合同地址
|
||||
*/
|
||||
@NotBlank(message = "合同地址不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String contractUrl;
|
||||
|
||||
/**
|
||||
* 合同名称 模糊查询
|
||||
*/
|
||||
@NotBlank(message = "合同名称不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String contractName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.qingyun.service.domain.bo;
|
||||
|
||||
import com.qingyun.service.domain.ContractRuleTemplate;
|
||||
import com.qingyun.common.core.domain.BaseEntity;
|
||||
import com.qingyun.common.core.validate.AddGroup;
|
||||
import com.qingyun.common.core.validate.EditGroup;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
|
||||
|
||||
/**
|
||||
* 规则要素模板业务对象 contract_rule_template
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = ContractRuleTemplate.class, reverseConvertGenerate = false)
|
||||
public class ContractRuleTemplateBo {
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
@NotNull(message = "模板ID不能为空", groups = { EditGroup.class })
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模板名称
|
||||
*/
|
||||
@NotBlank(message = "模板名称不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String templateName;
|
||||
|
||||
/**
|
||||
* 审查规则JSON
|
||||
*/
|
||||
@NotBlank(message = "审查规则JSON不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String reviewRuleJson;
|
||||
|
||||
/**
|
||||
* 审查要素JSON
|
||||
*/
|
||||
@NotBlank(message = "审查要素JSON不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String reviewElementJson;
|
||||
|
||||
/**
|
||||
* 原始文件地址
|
||||
*/
|
||||
//@NotBlank(message = "原始文件地址不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
//private String originalFileUrl;
|
||||
|
||||
/**
|
||||
* 状态 0 启用 1 停用
|
||||
*/
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.qingyun.service.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FpaReviewItem {
|
||||
|
||||
private String contractContent; // 合同内容 / 功能描述
|
||||
private String type; // 类型,如 ILF、EIF、EI、EO、EQ
|
||||
private String reviewBasis; // 评审依据 / 判定理由
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.qingyun.service.domain.dto;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Classname FunctionPointDto
|
||||
* @Author dyh
|
||||
* @Date 2025/9/25 11:16
|
||||
*/
|
||||
@Data
|
||||
public class FunctionPointDto {
|
||||
|
||||
/**
|
||||
* 功能模块
|
||||
*/
|
||||
@ExcelProperty(value = "模块名称")
|
||||
private String systemModule;
|
||||
|
||||
/**
|
||||
* 功能点
|
||||
*/
|
||||
@ExcelProperty(value = "功能点")
|
||||
private String feature;
|
||||
|
||||
/**
|
||||
* 功能点描述
|
||||
*/
|
||||
@ExcelProperty(value = "功能描述")
|
||||
private String desc;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.qingyun.service.domain.vo;
|
||||
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ContentStyle;
|
||||
import com.alibaba.excel.annotation.write.style.HeadStyle;
|
||||
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
|
||||
import com.qingyun.common.annotation.ExcelDictFormat;
|
||||
import com.qingyun.common.convert.ExcelDictConvert;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 对比结果视图对象 contract_compare_result
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@AutoMapper(target = ContractCompareResult.class)
|
||||
public class ContractCompareResultVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 结果ID
|
||||
*/
|
||||
@ExcelProperty(value = "结果ID")
|
||||
private Long resultId;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@ExcelProperty(value = "任务ID")
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 源文件定位
|
||||
*/
|
||||
private String baseBoxArea;
|
||||
/**
|
||||
* 源文件差异内容
|
||||
*/
|
||||
private String baseDiffContent;
|
||||
/**
|
||||
* 源文件差异类型(insert,update, delete)
|
||||
*/
|
||||
private String baseDiffType;
|
||||
/**
|
||||
* 源文件页码
|
||||
*/
|
||||
private Integer basePageNum;
|
||||
/**
|
||||
* 源文件行号
|
||||
*/
|
||||
private Integer baseLineNum;
|
||||
/**
|
||||
* 对比文件定位
|
||||
*/
|
||||
private String compareBoxArea;
|
||||
/**
|
||||
* 对比文件差异内容
|
||||
*/
|
||||
private String compareDiffContent;
|
||||
/**
|
||||
* 对比文件差异类型(insert,update, delete)
|
||||
*/
|
||||
private String compareDiffType;
|
||||
/**
|
||||
* 对比文件页码
|
||||
*/
|
||||
private Integer comparePageNum;
|
||||
/**
|
||||
* 对比文件行号
|
||||
*/
|
||||
private Integer compareLineNum;
|
||||
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.qingyun.service.domain.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ContentStyle;
|
||||
import com.alibaba.excel.annotation.write.style.HeadStyle;
|
||||
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 对比任务视图对象 contract_compare_task
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@AutoMapper(target = ContractCompareTask.class)
|
||||
public class ContractCompareTaskVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@ExcelProperty(value = "任务ID")
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 任务文件类型 pdf md 纯文本 txt ...
|
||||
*/
|
||||
private String taskType;
|
||||
|
||||
/**
|
||||
* 对比规则
|
||||
*/
|
||||
@ExcelProperty(value = "对比规则")
|
||||
private String compareRule;
|
||||
|
||||
/**
|
||||
* 第一份合同地址
|
||||
*/
|
||||
@ExcelProperty(value = "第一份合同地址")
|
||||
private String firstContractUrl;
|
||||
|
||||
/**
|
||||
* 第一份合同名称
|
||||
*/
|
||||
private String firstContractName;
|
||||
|
||||
/**
|
||||
* 第一份合同 宽度 pt
|
||||
*/
|
||||
private Float firstWidth;
|
||||
|
||||
/**
|
||||
* 第一份合同 高度 pt
|
||||
*/
|
||||
private Float firstHeight;
|
||||
|
||||
/**
|
||||
* 第一份合同页数
|
||||
*/
|
||||
private Integer firstPageNum;
|
||||
|
||||
/**
|
||||
* 第二份合同地址
|
||||
*/
|
||||
@ExcelProperty(value = "第二份合同地址")
|
||||
private String secondContractUrl;
|
||||
|
||||
/**
|
||||
* 第二份合同名称
|
||||
*/
|
||||
private String secondContractName;
|
||||
|
||||
/**
|
||||
* 第二份合同 宽度 pt
|
||||
*/
|
||||
private Float secondWidth;
|
||||
|
||||
/**
|
||||
* 第二份合同 高度 pt
|
||||
*/
|
||||
private Float secondHeight;
|
||||
|
||||
/**
|
||||
* 第二份合同页数
|
||||
*/
|
||||
private Integer secondPageNum;
|
||||
|
||||
/**
|
||||
* 第一份合同内容
|
||||
*/
|
||||
@ExcelProperty(value = "第一份合同内容")
|
||||
private String firstContractContent;
|
||||
|
||||
/**
|
||||
* 第二份合同内容
|
||||
*/
|
||||
@ExcelProperty(value = "第二份合同内容")
|
||||
private String secondContractContent;
|
||||
|
||||
/**
|
||||
* 状态(0:未开始 1:获取原文中 2:对比中 3:已完成 4:失败)
|
||||
*/
|
||||
@ExcelProperty(value = "状态(0:未开始 1:获取原文中 2:对比中 3:已完成 4:失败)")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
@ExcelProperty(value = "部门ID")
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@ExcelProperty(value = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.qingyun.service.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.qingyun.common.annotation.Sensitive;
|
||||
import com.qingyun.common.enums.SensitiveStrategy;
|
||||
import com.qingyun.service.domain.ContractModelConfig;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ContentStyle;
|
||||
import com.alibaba.excel.annotation.write.style.HeadStyle;
|
||||
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
|
||||
import com.qingyun.common.annotation.ExcelDictFormat;
|
||||
import com.qingyun.common.convert.ExcelDictConvert;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 模型配置视图对象 contract_model_config
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@AutoMapper(target = ContractModelConfig.class)
|
||||
public class ContractModelConfigVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 配置ID
|
||||
*/
|
||||
@ExcelProperty(value = "配置ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@ExcelProperty(value = "模型名称")
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 模型类型
|
||||
*/
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 模型地址
|
||||
*/
|
||||
@ExcelProperty(value = "模型地址")
|
||||
private String modelUrl;
|
||||
|
||||
/**
|
||||
* 模型版本
|
||||
*/
|
||||
private String modelVersion;
|
||||
|
||||
/**
|
||||
* 模型密钥 脱敏
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@ExcelProperty(value = "模型密钥")
|
||||
private String modelKey;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.qingyun.service.domain.vo;
|
||||
|
||||
import com.qingyun.service.domain.ContractReviewResult;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ContentStyle;
|
||||
import com.alibaba.excel.annotation.write.style.HeadStyle;
|
||||
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
|
||||
import com.qingyun.common.annotation.ExcelDictFormat;
|
||||
import com.qingyun.common.convert.ExcelDictConvert;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审查结果视图对象 contract_review_result
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@AutoMapper(target = ContractReviewResult.class)
|
||||
public class ContractReviewResultVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 结果ID
|
||||
*/
|
||||
@ExcelProperty(value = "结果ID")
|
||||
private Long resultId;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
@ExcelProperty(value = "任务ID")
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 合同原文
|
||||
*/
|
||||
@ExcelProperty(value = "合同原文")
|
||||
private String contractContent;
|
||||
|
||||
/**
|
||||
* 风险提示
|
||||
*/
|
||||
@ExcelProperty(value = "风险提示")
|
||||
private String riskTips;
|
||||
|
||||
/**
|
||||
* 风险等级 0:无风险 1:低风险 2:中风险 3:高风险
|
||||
*/
|
||||
@ExcelProperty(value = "风险等级")
|
||||
private String riskLevel;
|
||||
|
||||
/**
|
||||
* 审查依据
|
||||
*/
|
||||
@ExcelProperty(value = "审查依据")
|
||||
private String reviewBasis;
|
||||
|
||||
/**
|
||||
* 修改示例
|
||||
*/
|
||||
@ExcelProperty(value = "修改示例")
|
||||
private String modifyExample;
|
||||
|
||||
/**
|
||||
* 规格类型(1:规则 2:要素)
|
||||
*/
|
||||
@ExcelProperty(value = "规格类型(1:规则 2:要素)")
|
||||
private Long specType;
|
||||
|
||||
/**
|
||||
* 对应的合同页码
|
||||
*/
|
||||
private Integer pageNum;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.qingyun.service.domain.vo;
|
||||
|
||||
import com.qingyun.service.domain.ContractReviewTask;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ContentStyle;
|
||||
import com.alibaba.excel.annotation.write.style.HeadStyle;
|
||||
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
|
||||
import com.qingyun.common.annotation.ExcelDictFormat;
|
||||
import com.qingyun.common.convert.ExcelDictConvert;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 审查任务视图对象 contract_review_task
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-15
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@AutoMapper(target = ContractReviewTask.class)
|
||||
public class ContractReviewTaskVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
private Long taskId;
|
||||
|
||||
/**
|
||||
* 规则要素模板ID
|
||||
*/
|
||||
private Long templateId;
|
||||
|
||||
/**
|
||||
* 审查规则
|
||||
*/
|
||||
private String reviewRule;
|
||||
|
||||
/**
|
||||
* 审查要素
|
||||
*/
|
||||
private String reviewElement;
|
||||
|
||||
/**
|
||||
* 合同地址
|
||||
*/
|
||||
private String contractUrl;
|
||||
|
||||
/**
|
||||
* 合同原文MD
|
||||
*/
|
||||
private String contractContentMd;
|
||||
|
||||
/**
|
||||
* 合同图片
|
||||
*/
|
||||
private String contractPic;
|
||||
|
||||
/**
|
||||
* 页数
|
||||
*/
|
||||
private Integer pageNum;
|
||||
|
||||
/**
|
||||
* 状态(0:未开始 1:获取原文中 2:审查中 3:已审查 4:失败)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.qingyun.service.domain.vo;
|
||||
|
||||
import com.qingyun.service.domain.ContractRuleTemplate;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ContentStyle;
|
||||
import com.alibaba.excel.annotation.write.style.HeadStyle;
|
||||
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 规则要素模板视图对象 contract_rule_template
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
|
||||
@AutoMapper(target = ContractRuleTemplate.class)
|
||||
public class ContractRuleTemplateVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 模板ID
|
||||
*/
|
||||
@ExcelProperty(value = "模板ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模板名称
|
||||
*/
|
||||
@ExcelProperty(value = "模板名称")
|
||||
private String templateName;
|
||||
|
||||
/**
|
||||
* 审查规则JSON
|
||||
*/
|
||||
@ExcelProperty(value = "审查规则JSON")
|
||||
private String reviewRuleJson;
|
||||
|
||||
/**
|
||||
* 审查要素JSON
|
||||
*/
|
||||
@ExcelProperty(value = "审查要素JSON")
|
||||
private String reviewElementJson;
|
||||
|
||||
/**
|
||||
* 原始文件地址
|
||||
*/
|
||||
@ExcelProperty(value = "原始文件地址")
|
||||
private String originalFileUrl;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@ExcelProperty(value = "创建人")
|
||||
private String creator;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ExcelProperty(value = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 状态 0 启用 1 停用
|
||||
*/
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.qingyun.service.mapper;
|
||||
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.qingyun.service.domain.vo.ContractCompareResultVo;
|
||||
import com.qingyun.mybatisplus.core.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 对比结果Mapper接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface ContractCompareResultMapper extends BaseMapperPlus<ContractCompareResultMapper,ContractCompareResult, ContractCompareResultVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.qingyun.service.mapper;
|
||||
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import com.qingyun.service.domain.vo.ContractCompareTaskVo;
|
||||
import com.qingyun.mybatisplus.core.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 对比任务Mapper接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface ContractCompareTaskMapper extends BaseMapperPlus<ContractCompareTaskMapper,ContractCompareTask, ContractCompareTaskVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.qingyun.service.mapper;
|
||||
|
||||
import com.qingyun.service.domain.ContractModelConfig;
|
||||
import com.qingyun.service.domain.vo.ContractModelConfigVo;
|
||||
import com.qingyun.mybatisplus.core.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 模型配置Mapper接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface ContractModelConfigMapper extends BaseMapperPlus<ContractModelConfigMapper,ContractModelConfig, ContractModelConfigVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.qingyun.service.mapper;
|
||||
|
||||
import com.qingyun.service.domain.ContractReviewResult;
|
||||
import com.qingyun.service.domain.vo.ContractReviewResultVo;
|
||||
import com.qingyun.mybatisplus.core.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 审查结果Mapper接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface ContractReviewResultMapper extends BaseMapperPlus<ContractReviewResultMapper,ContractReviewResult, ContractReviewResultVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.qingyun.service.mapper;
|
||||
|
||||
import com.qingyun.service.domain.ContractReviewTask;
|
||||
import com.qingyun.service.domain.vo.ContractReviewTaskVo;
|
||||
import com.qingyun.mybatisplus.core.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 审查任务Mapper接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-15
|
||||
*/
|
||||
public interface ContractReviewTaskMapper extends BaseMapperPlus<ContractReviewTaskMapper,ContractReviewTask, ContractReviewTaskVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.qingyun.service.mapper;
|
||||
|
||||
import com.qingyun.service.domain.ContractRuleTemplate;
|
||||
import com.qingyun.service.domain.vo.ContractRuleTemplateVo;
|
||||
import com.qingyun.mybatisplus.core.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 规则要素模板Mapper接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface ContractRuleTemplateMapper extends BaseMapperPlus<ContractRuleTemplateMapper,ContractRuleTemplate, ContractRuleTemplateVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.qingyun.service.service;
|
||||
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.qingyun.service.domain.vo.ContractCompareResultVo;
|
||||
import com.qingyun.service.domain.bo.ContractCompareResultBo;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.qingyun.mybatisplus.core.IServicePlus;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对比结果Service接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface IContractCompareResultService extends IServicePlus<ContractCompareResult, ContractCompareResultVo> {
|
||||
|
||||
/**
|
||||
* 查询对比结果列表
|
||||
*/
|
||||
List<ContractCompareResultVo> queryList(Long taskId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.qingyun.service.service;
|
||||
|
||||
import com.qingyun.service.compare.strategy.FileComparisonStrategy;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import com.qingyun.service.domain.vo.ContractCompareTaskVo;
|
||||
import com.qingyun.service.domain.bo.ContractCompareTaskBo;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.qingyun.mybatisplus.core.IServicePlus;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对比任务Service接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface IContractCompareTaskService extends IServicePlus<ContractCompareTask, ContractCompareTaskVo> {
|
||||
|
||||
/**
|
||||
* 查询对比任务
|
||||
*/
|
||||
ContractCompareTaskVo queryById(Long taskId);
|
||||
|
||||
/**
|
||||
* 查询对比任务列表
|
||||
*/
|
||||
TableDataInfo<ContractCompareTaskVo> queryPageList(String contractName, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询对比任务列表
|
||||
*/
|
||||
List<ContractCompareTaskVo> queryList(ContractCompareTaskBo bo);
|
||||
|
||||
/**
|
||||
* 新增对比任务
|
||||
*/
|
||||
Long insertByBo(ContractCompareTaskBo bo);
|
||||
|
||||
/**
|
||||
* 修改对比任务
|
||||
*/
|
||||
Boolean updateByBo(ContractCompareTaskBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除对比任务信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
* @param taskId
|
||||
* @return
|
||||
*/
|
||||
void executeTask(FileComparisonStrategy strategy, ContractCompareTask task);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.qingyun.service.service;
|
||||
|
||||
import com.qingyun.service.domain.ContractModelConfig;
|
||||
import com.qingyun.service.domain.vo.ContractModelConfigVo;
|
||||
import com.qingyun.service.domain.bo.ContractModelConfigBo;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.qingyun.mybatisplus.core.IServicePlus;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 模型配置Service接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface IContractModelConfigService extends IServicePlus<ContractModelConfig, ContractModelConfigVo> {
|
||||
|
||||
/**
|
||||
* 查询模型配置
|
||||
*/
|
||||
ContractModelConfigVo queryById(String id);
|
||||
|
||||
/**
|
||||
* 查询模型配置列表
|
||||
*/
|
||||
TableDataInfo<ContractModelConfigVo> queryPageList(ContractModelConfigBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询模型配置列表
|
||||
*/
|
||||
List<ContractModelConfigVo> queryList(ContractModelConfigBo bo);
|
||||
|
||||
/**
|
||||
* 新增模型配置
|
||||
*/
|
||||
Boolean insertByBo(ContractModelConfigBo bo);
|
||||
|
||||
/**
|
||||
* 修改模型配置
|
||||
*/
|
||||
Boolean updateByBo(ContractModelConfigBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除模型配置信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Long[] ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.qingyun.service.service;
|
||||
|
||||
import com.qingyun.service.domain.ContractReviewResult;
|
||||
import com.qingyun.service.domain.vo.ContractReviewResultVo;
|
||||
import com.qingyun.service.domain.bo.ContractReviewResultBo;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.qingyun.mybatisplus.core.IServicePlus;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审查结果Service接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface IContractReviewResultService extends IServicePlus<ContractReviewResult, ContractReviewResultVo> {
|
||||
|
||||
/**
|
||||
* 查询审查结果
|
||||
*/
|
||||
ContractReviewResultVo queryById(String resultId);
|
||||
|
||||
|
||||
/**
|
||||
* 查询审查结果列表
|
||||
*/
|
||||
List<ContractReviewResultVo> queryList(Long taskId);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.qingyun.service.service;
|
||||
|
||||
import com.qingyun.service.domain.ContractReviewTask;
|
||||
import com.qingyun.service.domain.vo.ContractReviewTaskVo;
|
||||
import com.qingyun.service.domain.bo.ContractReviewTaskBo;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.qingyun.mybatisplus.core.IServicePlus;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审查任务Service接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-15
|
||||
*/
|
||||
public interface IContractReviewTaskService extends IServicePlus<ContractReviewTask, ContractReviewTaskVo> {
|
||||
|
||||
/**
|
||||
* 查询审查任务
|
||||
*/
|
||||
ContractReviewTaskVo queryById(Long taskId);
|
||||
|
||||
/**
|
||||
* 查询审查任务列表
|
||||
*/
|
||||
TableDataInfo<ContractReviewTaskVo> queryPageList(ContractReviewTaskBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询审查任务列表
|
||||
*/
|
||||
List<ContractReviewTaskVo> queryList(ContractReviewTaskBo bo);
|
||||
|
||||
/**
|
||||
* 新增审查任务
|
||||
*/
|
||||
Boolean insertByBo(ContractReviewTaskBo bo);
|
||||
|
||||
/**
|
||||
* 修改审查任务
|
||||
*/
|
||||
Boolean updateByBo(ContractReviewTaskBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除审查任务信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
void execute(ContractReviewTask contractReviewTask, Long modelId);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.qingyun.service.service;
|
||||
|
||||
import com.qingyun.service.domain.ContractRuleTemplate;
|
||||
import com.qingyun.service.domain.vo.ContractRuleTemplateVo;
|
||||
import com.qingyun.service.domain.bo.ContractRuleTemplateBo;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.qingyun.mybatisplus.core.IServicePlus;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 规则要素模板Service接口
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
public interface IContractRuleTemplateService extends IServicePlus<ContractRuleTemplate, ContractRuleTemplateVo> {
|
||||
|
||||
/**
|
||||
* 查询规则要素模板
|
||||
*/
|
||||
ContractRuleTemplateVo queryById(String id);
|
||||
|
||||
/**
|
||||
* 查询规则要素模板列表
|
||||
*/
|
||||
TableDataInfo<ContractRuleTemplateVo> queryPageList(ContractRuleTemplateBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询规则要素模板列表
|
||||
*/
|
||||
List<ContractRuleTemplateVo> queryList(ContractRuleTemplateBo bo);
|
||||
|
||||
/**
|
||||
* 新增规则要素模板
|
||||
*/
|
||||
Boolean insertByBo(ContractRuleTemplateBo bo);
|
||||
|
||||
/**
|
||||
* 修改规则要素模板
|
||||
*/
|
||||
Boolean updateByBo(ContractRuleTemplateBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除规则要素模板信息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.qingyun.service.service.impl;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.qingyun.common.utils.MapstructUtils;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.qingyun.service.domain.bo.ContractCompareResultBo;
|
||||
import com.qingyun.service.domain.vo.ContractCompareResultVo;
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.qingyun.service.mapper.ContractCompareResultMapper;
|
||||
import com.qingyun.service.service.IContractCompareResultService;
|
||||
|
||||
import com.qingyun.mybatisplus.core.ServicePlusImpl;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 对比结果Service业务层处理
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ContractCompareResultServiceImpl extends ServicePlusImpl<ContractCompareResultMapper, ContractCompareResult, ContractCompareResultVo> implements IContractCompareResultService {
|
||||
|
||||
|
||||
/**
|
||||
* 查询对比结果列表
|
||||
* @param taskId 任务ID
|
||||
*/
|
||||
@Override
|
||||
public List<ContractCompareResultVo> queryList(Long taskId) {
|
||||
List<ContractCompareResult> resultList = this.baseMapper.selectList(Wrappers.lambdaQuery(ContractCompareResult.class)
|
||||
.eq(ContractCompareResult::getTaskId, taskId)
|
||||
);
|
||||
return MapstructUtils.convert(resultList, ContractCompareResultVo.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.qingyun.service.service.impl;
|
||||
|
||||
import com.github.difflib.patch.DeltaType;
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import com.qingyun.common.helper.LoginHelper;
|
||||
import com.qingyun.service.compare.*;
|
||||
import com.qingyun.service.compare.factory.FileComparisonStrategyFactory;
|
||||
import com.qingyun.service.compare.strategy.FileComparisonStrategy;
|
||||
import com.qingyun.service.domain.ContractCompareResult;
|
||||
import com.qingyun.service.mapper.ContractCompareResultMapper;
|
||||
import com.qingyun.service.utils.PdfUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.qingyun.common.utils.MapstructUtils;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.qingyun.service.domain.bo.ContractCompareTaskBo;
|
||||
import com.qingyun.service.domain.vo.ContractCompareTaskVo;
|
||||
import com.qingyun.service.domain.ContractCompareTask;
|
||||
import com.qingyun.service.mapper.ContractCompareTaskMapper;
|
||||
import com.qingyun.service.service.IContractCompareTaskService;
|
||||
|
||||
import com.qingyun.mybatisplus.core.ServicePlusImpl;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 对比任务Service业务层处理
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
@Slf4j
|
||||
@SuppressWarnings("all")
|
||||
public class ContractCompareTaskServiceImpl extends ServicePlusImpl<ContractCompareTaskMapper, ContractCompareTask, ContractCompareTaskVo> implements IContractCompareTaskService {
|
||||
|
||||
private final ContractCompareResultMapper compareResultMapper;
|
||||
|
||||
private final PdfUtil pdfUtil;
|
||||
|
||||
private final PdfDiffWithPosition pdfDiffWithPosition;
|
||||
|
||||
private final FileComparisonStrategyFactory strategyFactory;
|
||||
|
||||
private final SqlSessionFactory sqlSessionFactory;
|
||||
|
||||
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(5);
|
||||
|
||||
/**
|
||||
* 查询对比任务
|
||||
*/
|
||||
@Override
|
||||
public ContractCompareTaskVo queryById(Long taskId) {
|
||||
ContractCompareTaskVo contractCompareTaskVo = baseMapper.selectVoById(taskId);
|
||||
return contractCompareTaskVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询对比任务列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ContractCompareTaskVo> queryPageList(String contractName, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ContractCompareTask> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(contractName), ContractCompareTask::getFirstContractName, contractName)
|
||||
.or()
|
||||
.like(StringUtils.isNotBlank(contractName), ContractCompareTask::getSecondContractName, contractName);
|
||||
// 创建时间降序
|
||||
lqw.orderByDesc(ContractCompareTask::getCreateTime);
|
||||
Page<ContractCompareTaskVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询对比任务列表
|
||||
*/
|
||||
@Override
|
||||
public List<ContractCompareTaskVo> queryList(ContractCompareTaskBo bo) {
|
||||
LambdaQueryWrapper<ContractCompareTask> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ContractCompareTask> buildQueryWrapper(ContractCompareTaskBo bo) {
|
||||
LambdaQueryWrapper<ContractCompareTask> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getCompareRule()), ContractCompareTask::getCompareRule, bo.getCompareRule());
|
||||
// 创建时间降序
|
||||
lqw.orderByDesc(ContractCompareTask::getCreateTime);
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增对比任务
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long insertByBo(ContractCompareTaskBo bo) {
|
||||
ContractCompareTask add = MapstructUtils.convert(bo, ContractCompareTask.class);
|
||||
add.setStatus(2);
|
||||
add.setUserId(LoginHelper.getUserId());
|
||||
add.setDeptId(LoginHelper.getDeptId());
|
||||
add.setCreateTime(new Date());
|
||||
add.setCreateBy(LoginHelper.getUsername());
|
||||
int insert = baseMapper.insert(add);
|
||||
if (insert < 1) {
|
||||
throw new ServiceException("新增失败");
|
||||
}
|
||||
return add.getTaskId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改对比任务
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ContractCompareTaskBo bo) {
|
||||
ContractCompareTask update = MapstructUtils.convert(bo, ContractCompareTask.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ContractCompareTask entity) {
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除对比任务
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if (isValid) {
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
*
|
||||
* @param taskId
|
||||
* @return
|
||||
*/
|
||||
@Async
|
||||
@Override
|
||||
public void executeTask(FileComparisonStrategy strategy, ContractCompareTask task) {
|
||||
Future<?> future = EXECUTOR.submit(() -> {
|
||||
try {
|
||||
// 比对
|
||||
List<DiffRegion> diff = strategy.compare(task);
|
||||
// 组装并保存结果
|
||||
strategy.assembleAndSaveResult(task, diff);
|
||||
} catch (Exception e) {
|
||||
log.error("任务执行失败: {}", task.getTaskId(), e);
|
||||
task.setStatus(4);
|
||||
task.setErrorMsg("执行异常: " + e.getMessage());
|
||||
baseMapper.updateById(task);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
future.get(600, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
log.warn("任务超时: {}", task.getTaskId());
|
||||
task.setStatus(4);
|
||||
task.setErrorMsg("任务执行超时");
|
||||
baseMapper.updateById(task);
|
||||
future.cancel(true);
|
||||
} catch (Exception e) {
|
||||
log.error("任务执行异常", e);
|
||||
task.setStatus(4);
|
||||
task.setErrorMsg("系统异常");
|
||||
baseMapper.updateById(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.qingyun.service.service.impl;
|
||||
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import com.qingyun.common.helper.LoginHelper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.qingyun.common.utils.MapstructUtils;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.qingyun.service.domain.bo.ContractModelConfigBo;
|
||||
import com.qingyun.service.domain.vo.ContractModelConfigVo;
|
||||
import com.qingyun.service.domain.ContractModelConfig;
|
||||
import com.qingyun.service.mapper.ContractModelConfigMapper;
|
||||
import com.qingyun.service.service.IContractModelConfigService;
|
||||
|
||||
import com.qingyun.mybatisplus.core.ServicePlusImpl;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 模型配置Service业务层处理
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ContractModelConfigServiceImpl extends ServicePlusImpl<ContractModelConfigMapper, ContractModelConfig, ContractModelConfigVo> implements IContractModelConfigService {
|
||||
|
||||
|
||||
/**
|
||||
* 查询模型配置
|
||||
*/
|
||||
@Override
|
||||
public ContractModelConfigVo queryById(String id){
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询模型配置列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ContractModelConfigVo> queryPageList(ContractModelConfigBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ContractModelConfig> lqw = buildQueryWrapper(bo);
|
||||
Page<ContractModelConfigVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
result.getRecords().forEach(item -> {
|
||||
// 密钥脱敏 只显示前3位,后面用 * 号替换
|
||||
if (item != null && item.getModelKey() != null) {
|
||||
String originalKey = item.getModelKey();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (originalKey.length() > 3) {
|
||||
sb.append(originalKey, 0, 3);
|
||||
for (int i = 3; i < originalKey.length(); i++) {
|
||||
sb.append("*");
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < originalKey.length(); i++) {
|
||||
sb.append("*");
|
||||
}
|
||||
}
|
||||
item.setModelKey(sb.toString());
|
||||
}
|
||||
});
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询模型配置列表
|
||||
*/
|
||||
@Override
|
||||
public List<ContractModelConfigVo> queryList(ContractModelConfigBo bo) {
|
||||
LambdaQueryWrapper<ContractModelConfig> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ContractModelConfig> buildQueryWrapper(ContractModelConfigBo bo) {
|
||||
LambdaQueryWrapper<ContractModelConfig> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(bo.getModelName()), ContractModelConfig::getModelName, bo.getModelName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelUrl()), ContractModelConfig::getModelUrl, bo.getModelUrl());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelKey()), ContractModelConfig::getModelKey, bo.getModelKey());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelType()), ContractModelConfig::getModelType, bo.getModelType());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增模型配置
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean insertByBo(ContractModelConfigBo bo) {
|
||||
ContractModelConfig add = MapstructUtils.convert(bo, ContractModelConfig.class);
|
||||
validEntityBeforeSave(add);
|
||||
add.setCreateBy(LoginHelper.getUsername());
|
||||
add.setCreateTime(new Date());
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改模型配置
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ContractModelConfigBo bo) {
|
||||
ContractModelConfig update = MapstructUtils.convert(bo, ContractModelConfig.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ContractModelConfig entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
// 校验模型名称是否唯一
|
||||
Long count;
|
||||
if (entity.getId() == null) {
|
||||
count = this.baseMapper.selectCount(Wrappers.<ContractModelConfig>lambdaQuery()
|
||||
.eq(ContractModelConfig::getModelName, entity.getModelName())
|
||||
);
|
||||
} else {
|
||||
count = this.baseMapper.selectCount(Wrappers.<ContractModelConfig>lambdaQuery()
|
||||
.eq(ContractModelConfig::getModelName, entity.getModelName())
|
||||
.ne(ContractModelConfig::getId, entity.getId())
|
||||
);
|
||||
}
|
||||
if (count > 0) {
|
||||
throw new ServiceException("模型名称已存在");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除模型配置
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Long[] ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(Arrays.asList(ids)) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.qingyun.service.service.impl;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.qingyun.common.utils.MapstructUtils;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.qingyun.service.domain.bo.ContractReviewResultBo;
|
||||
import com.qingyun.service.domain.vo.ContractReviewResultVo;
|
||||
import com.qingyun.service.domain.ContractReviewResult;
|
||||
import com.qingyun.service.mapper.ContractReviewResultMapper;
|
||||
import com.qingyun.service.service.IContractReviewResultService;
|
||||
|
||||
import com.qingyun.mybatisplus.core.ServicePlusImpl;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 审查结果Service业务层处理
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ContractReviewResultServiceImpl extends ServicePlusImpl<ContractReviewResultMapper, ContractReviewResult, ContractReviewResultVo> implements IContractReviewResultService {
|
||||
|
||||
|
||||
/**
|
||||
* 查询审查结果
|
||||
*/
|
||||
@Override
|
||||
public ContractReviewResultVo queryById(String resultId){
|
||||
return baseMapper.selectVoById(resultId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询审查结果列表
|
||||
*/
|
||||
@Override
|
||||
public List<ContractReviewResultVo> queryList(Long taskId) {
|
||||
List<ContractReviewResult> resultList = this.baseMapper.selectList(Wrappers.lambdaQuery(ContractReviewResult.class)
|
||||
.eq(ContractReviewResult::getTaskId, taskId)
|
||||
.orderByAsc(ContractReviewResult::getPageNum)
|
||||
);
|
||||
return MapstructUtils.convert(resultList, ContractReviewResultVo.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.qingyun.service.service.impl;
|
||||
|
||||
import com.qingyun.common.exception.ServiceException;
|
||||
import com.qingyun.common.utils.StringUtils;
|
||||
import com.qingyun.service.compare.service.ReviewTaskService;
|
||||
import com.qingyun.service.mapper.ContractRuleTemplateMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import com.qingyun.common.utils.MapstructUtils;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.qingyun.service.domain.bo.ContractReviewTaskBo;
|
||||
import com.qingyun.service.domain.vo.ContractReviewTaskVo;
|
||||
import com.qingyun.service.domain.ContractReviewTask;
|
||||
import com.qingyun.service.mapper.ContractReviewTaskMapper;
|
||||
import com.qingyun.service.service.IContractReviewTaskService;
|
||||
|
||||
import com.qingyun.mybatisplus.core.ServicePlusImpl;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* 审查任务Service业务层处理
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-15
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
@SuppressWarnings("all")
|
||||
public class ContractReviewTaskServiceImpl extends ServicePlusImpl<ContractReviewTaskMapper, ContractReviewTask, ContractReviewTaskVo> implements IContractReviewTaskService {
|
||||
|
||||
private final ContractRuleTemplateMapper contractRuleTemplateMapper;
|
||||
|
||||
private final ReviewTaskService contractReviewService;
|
||||
|
||||
|
||||
/**
|
||||
* 查询审查任务
|
||||
*/
|
||||
@Override
|
||||
public ContractReviewTaskVo queryById(Long taskId){
|
||||
ContractReviewTaskVo vo = baseMapper.selectVoById(taskId);
|
||||
if (vo == null) {
|
||||
throw new ServiceException("审查任务不存在");
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询审查任务列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ContractReviewTaskVo> queryPageList(ContractReviewTaskBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ContractReviewTask> lqw = buildQueryWrapper(bo);
|
||||
Page<ContractReviewTaskVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询审查任务列表
|
||||
*/
|
||||
@Override
|
||||
public List<ContractReviewTaskVo> queryList(ContractReviewTaskBo bo) {
|
||||
LambdaQueryWrapper<ContractReviewTask> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ContractReviewTask> buildQueryWrapper(ContractReviewTaskBo bo) {
|
||||
LambdaQueryWrapper<ContractReviewTask> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(bo.getContractName()), ContractReviewTask::getContractName, bo.getContractName());
|
||||
lqw.orderByDesc(ContractReviewTask::getCreateTime);
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增审查任务
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ContractReviewTaskBo bo) {
|
||||
ContractReviewTask add = MapstructUtils.convert(bo, ContractReviewTask.class);
|
||||
add.setUserId(1L);
|
||||
add.setDeptId(1L);
|
||||
add.setStatus(0);
|
||||
add.setCreateBy("admin");
|
||||
add.setCreateTime(new Date());
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setTaskId(add.getTaskId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改审查任务
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ContractReviewTaskBo bo) {
|
||||
ContractReviewTask update = MapstructUtils.convert(bo, ContractReviewTask.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ContractReviewTask entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除审查任务
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
@Async
|
||||
@Override
|
||||
public void execute(ContractReviewTask contractReviewTask, Long modelId) {
|
||||
contractReviewService.execute(contractReviewTask, modelId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.qingyun.service.service.impl;
|
||||
|
||||
import com.qingyun.common.helper.LoginHelper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.qingyun.common.utils.MapstructUtils;
|
||||
import com.qingyun.mybatisplus.page.TableDataInfo;
|
||||
import com.qingyun.common.core.domain.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.qingyun.service.domain.bo.ContractRuleTemplateBo;
|
||||
import com.qingyun.service.domain.vo.ContractRuleTemplateVo;
|
||||
import com.qingyun.service.domain.ContractRuleTemplate;
|
||||
import com.qingyun.service.mapper.ContractRuleTemplateMapper;
|
||||
import com.qingyun.service.service.IContractRuleTemplateService;
|
||||
|
||||
import com.qingyun.mybatisplus.core.ServicePlusImpl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 规则要素模板Service业务层处理
|
||||
*
|
||||
* @author jianlu
|
||||
* @date 2025-07-11
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ContractRuleTemplateServiceImpl extends ServicePlusImpl<ContractRuleTemplateMapper, ContractRuleTemplate, ContractRuleTemplateVo> implements IContractRuleTemplateService {
|
||||
|
||||
|
||||
/**
|
||||
* 查询规则要素模板
|
||||
*/
|
||||
@Override
|
||||
public ContractRuleTemplateVo queryById(String id){
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询规则要素模板列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ContractRuleTemplateVo> queryPageList(ContractRuleTemplateBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ContractRuleTemplate> lqw = buildQueryWrapper(bo);
|
||||
Page<ContractRuleTemplateVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询规则要素模板列表
|
||||
*/
|
||||
@Override
|
||||
public List<ContractRuleTemplateVo> queryList(ContractRuleTemplateBo bo) {
|
||||
LambdaQueryWrapper<ContractRuleTemplate> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ContractRuleTemplate> buildQueryWrapper(ContractRuleTemplateBo bo) {
|
||||
LambdaQueryWrapper<ContractRuleTemplate> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(bo.getTemplateName()), ContractRuleTemplate::getTemplateName, bo.getTemplateName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getReviewRuleJson()), ContractRuleTemplate::getReviewRuleJson, bo.getReviewRuleJson());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getReviewElementJson()), ContractRuleTemplate::getReviewElementJson, bo.getReviewElementJson());
|
||||
lqw.eq(ContractRuleTemplate::getStatus, 0);
|
||||
lqw.orderByDesc(ContractRuleTemplate::getCreateTime);
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增规则要素模板
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ContractRuleTemplateBo bo) {
|
||||
ContractRuleTemplate add = MapstructUtils.convert(bo, ContractRuleTemplate.class);
|
||||
if (add == null) {
|
||||
return false;
|
||||
}
|
||||
add.setCreateBy(String.valueOf(LoginHelper.getUserId()));;
|
||||
add.setCreateTime(new Date());
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改规则要素模板
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ContractRuleTemplateBo bo) {
|
||||
ContractRuleTemplate update = MapstructUtils.convert(bo, ContractRuleTemplate.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ContractRuleTemplate entity){
|
||||
//TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除规则要素模板
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.qingyun.service.utils;
|
||||
|
||||
import java.util.regex.*;
|
||||
import java.util.*;
|
||||
|
||||
public class MarkdownUtils {
|
||||
|
||||
/**
|
||||
* 从 Markdown 文本中提取所有用 ```json 包裹的内容;
|
||||
* 如果整段文本没有代码块包裹,则直接将其视为一段 JSON 返回。
|
||||
*
|
||||
* @param markdown Markdown 格式文本或裸 JSON 字符串
|
||||
* @return 提取到的所有 JSON 字符串列表(至少包含一项)
|
||||
*/
|
||||
public static List<String> extractJsonBlocks(String markdown) {
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
// 1. 快速判断是否存在 ```json 代码块包裹
|
||||
String trimmed = markdown.trim();
|
||||
boolean hasJsonBlock = trimmed.contains("```json");
|
||||
|
||||
if (!hasJsonBlock) {
|
||||
// 2. 没有代码块,直接把整个文本当作一份 JSON
|
||||
result.add(trimmed);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 3. 存在代码块,使用正则提取
|
||||
String regex = "```json\\s*([\\s\\S]*?)\\s*```";
|
||||
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(trimmed);
|
||||
|
||||
while (matcher.find()) {
|
||||
String jsonContent = matcher.group(1).trim();
|
||||
if (!jsonContent.isEmpty()) {
|
||||
result.add(jsonContent);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 极端场景:文本里有 ```json 却匹配不到任何内容,返回空列表
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用版本:支持指定语言标识(如 json、java、python 等)
|
||||
*
|
||||
* @param markdown Markdown 格式文本
|
||||
* @param language 指定语言标识,如 "json"
|
||||
* @return 提取到的代码块内容列表
|
||||
*/
|
||||
public static List<String> extractCodeBlocksByLanguage(String markdown, String language) {
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
String regex = String.format("```%s\\s*([\\s\\S]*?)\\s*```", Pattern.quote(language));
|
||||
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(markdown);
|
||||
|
||||
while (matcher.find()) {
|
||||
String codeContent = matcher.group(1).trim();
|
||||
result.add(codeContent);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取所有形式的代码块(不限制语言类型)
|
||||
*
|
||||
* @param markdown Markdown 格式文本
|
||||
* @return 所有代码块内容列表
|
||||
*/
|
||||
public static List<String> extractAllCodeBlocks(String markdown) {
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
String regex = "```[a-zA-Z]*\\s*([\\s\\S]*?)\\s*```";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(markdown);
|
||||
|
||||
while (matcher.find()) {
|
||||
String codeContent = matcher.group(1).trim();
|
||||
result.add(codeContent);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package com.qingyun.service.utils;
|
||||
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.documents4j.api.DocumentType;
|
||||
import com.documents4j.api.IConverter;
|
||||
import com.documents4j.job.LocalConverter;
|
||||
import com.qingyun.common.config.QingYunConfig;
|
||||
import com.qingyun.common.utils.file.FileUploadUtils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
public class PdfUtil {
|
||||
|
||||
@Value("${server.port}")
|
||||
private String port;
|
||||
|
||||
/**
|
||||
* @param source 原文件
|
||||
* @param desFilePath 生成图片的路径
|
||||
* @param desFileName 生成图片的名称(多页文档时会变成:名称+下划线+从1开始的数字)
|
||||
* @return
|
||||
*/
|
||||
public Pair<Boolean, Object> pdfToImage(File source, String desFilePath, String desFileName) throws IOException {
|
||||
//通过给定的源路径名字符串创建一个File实例
|
||||
|
||||
if (!source.exists()) {
|
||||
return Pair.of(false, "文件不存在,无法转化");
|
||||
}
|
||||
//目录不存在则创建目录
|
||||
File destination = new File(desFilePath);
|
||||
if (!destination.exists()) {
|
||||
boolean flag = destination.mkdirs();
|
||||
System.out.println("创建文件夹结果:" + flag);
|
||||
}
|
||||
PDDocument doc = null;
|
||||
try {
|
||||
//加载PDF文件
|
||||
doc = Loader.loadPDF(source);
|
||||
PDFRenderer renderer = new PDFRenderer(doc);
|
||||
//获取PDF文档的页数
|
||||
int pageCount = doc.getNumberOfPages();
|
||||
System.out.println("文档一共" + pageCount + "页");
|
||||
List<Object> fileList = new ArrayList<>();
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
// 只有一页的时候文件名为传入的文件名,大于一页的文件名为:文件名_自增加数字(从1开始)
|
||||
String realFileName = pageCount > 1 ? desFileName + "_" + (i + 1) : desFileName;
|
||||
// 每一页通过分辨率和颜色值进行转化
|
||||
BufferedImage bufferedImage = renderer.renderImageWithDPI(i, 150, ImageType.RGB);
|
||||
String relativePath = FileUploadUtils.getPathFileName(QingYunConfig.getUploadPath() + "/" + desFilePath, realFileName + "." + "png");
|
||||
String filePath = QingYunConfig.getUploadPath() + "/" + desFilePath + "/" + realFileName + "." + "png";
|
||||
// 确保目录存在
|
||||
File imageFile = new File(filePath);
|
||||
if (!imageFile.getParentFile().exists()) {
|
||||
boolean mkdirs = imageFile.getParentFile().mkdirs(); // 创建父目录
|
||||
if (!mkdirs) {
|
||||
return Pair.of(false, "无法创建目录:" + imageFile.getParentFile().getAbsolutePath());
|
||||
}
|
||||
}
|
||||
// 写入文件
|
||||
ImageIO.write(bufferedImage, "png", imageFile);
|
||||
|
||||
// 文件名存入 list
|
||||
fileList.add(relativePath);
|
||||
}
|
||||
return Pair.of(true, fileList);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return Pair.of(false, "PDF转化图片异常");
|
||||
} finally {
|
||||
try {
|
||||
if (doc != null) {
|
||||
doc.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.out.println("关闭文档失败");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public File downloadFile(String pdfUrl, String name) throws IOException {
|
||||
// name 去掉文件后缀
|
||||
String newName = name.substring(0, name.lastIndexOf("."));
|
||||
String projectUrl = QingYunConfig.getProjectUrl();
|
||||
File tempFile = File.createTempFile(newName + System.currentTimeMillis(), getFileExtension(name));
|
||||
long file = HttpUtil.downloadFile(projectUrl + pdfUrl, tempFile);
|
||||
if (file <= 0) {
|
||||
throw new IOException("无法下载 PDF 文件");
|
||||
}
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
public File convertWordToPdf(File wordFile) throws IOException, InterruptedException {
|
||||
if (!wordFile.exists() || !wordFile.isFile()) {
|
||||
throw new IOException("Word文件不存在或无效: " + wordFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
|
||||
if (osName.contains("win")) {
|
||||
// Windows 系统使用 Apache POI 实现 Word 转 PDF
|
||||
return convertWordToPdfWindows(wordFile);
|
||||
} else {
|
||||
// Linux 或其他系统使用 LibreOffice 实现 Word 转 PDF
|
||||
return convertWordToPdfLinux(wordFile);
|
||||
}
|
||||
}
|
||||
|
||||
private File convertWordToPdfWindows(File wordFile) throws IOException {
|
||||
File outputFile = new File(wordFile.getParent(),
|
||||
FilenameUtils.getBaseName(wordFile.getName()) + ".pdf");
|
||||
|
||||
try {
|
||||
IConverter converter = LocalConverter.builder().build();
|
||||
|
||||
Future<Boolean> conversion = converter
|
||||
.convert(wordFile).as(DocumentType.MS_WORD)
|
||||
.to(outputFile).as(DocumentType.PDF)
|
||||
.prioritizeWith(1000) // optional
|
||||
.schedule();
|
||||
conversion.get(); // 这里会阻塞直到转换完成
|
||||
return outputFile;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Word 转 PDF 失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private File convertWordToPdfLinux(File wordFile) throws IOException, InterruptedException {
|
||||
// 保留现有的 Linux 实现逻辑
|
||||
if (!isLibreOfficeInstalled()) {
|
||||
throw new IOException("LibreOffice未安装! 请先执行: sudo apt-get install libreoffice");
|
||||
}
|
||||
|
||||
File outputDir = wordFile.getParentFile();
|
||||
String pdfName = FilenameUtils.getBaseName(wordFile.getName()) + ".pdf";
|
||||
File outputFile = new File(outputDir, pdfName);
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"/usr/bin/soffice",
|
||||
"--headless",
|
||||
"--convert-to", "pdf",
|
||||
"--outdir", outputDir.getAbsolutePath(),
|
||||
wordFile.getAbsolutePath()
|
||||
);
|
||||
|
||||
pb.redirectErrorStream(true);
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
|
||||
|
||||
Process process = pb.start();
|
||||
boolean completed = process.waitFor(5, TimeUnit.MINUTES);
|
||||
|
||||
if (!completed) {
|
||||
process.destroyForcibly();
|
||||
throw new IOException("转换超时(5分钟)");
|
||||
}
|
||||
|
||||
if (process.exitValue() != 0) {
|
||||
throw new IOException("LibreOffice转换失败,错误码: " + process.exitValue());
|
||||
}
|
||||
|
||||
if (!outputFile.exists() || outputFile.length() == 0) {
|
||||
throw new IOException("PDF文件未生成或为空");
|
||||
}
|
||||
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
private boolean isLibreOfficeInstalled() {
|
||||
try {
|
||||
Process p = Runtime.getRuntime().exec("which soffice");
|
||||
return p.waitFor(2, TimeUnit.SECONDS) && p.exitValue() == 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
* @param fileName 文件名
|
||||
* @return 文件扩展名(包括点号)
|
||||
*/
|
||||
private String getFileExtension(String fileName) {
|
||||
if (fileName == null || fileName.lastIndexOf('.') == -1) {
|
||||
return ""; // 没有扩展名
|
||||
}
|
||||
return fileName.substring(fileName.lastIndexOf('.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingyun.service.mapper.ContractCompareResultMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingyun.service.mapper.ContractCompareTaskMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingyun.service.mapper.ContractModelConfigMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingyun.service.mapper.ContractReviewResultMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingyun.service.mapper.ContractReviewTaskMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingyun.service.mapper.ContractRuleTemplateMapper">
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user