first commit

This commit is contained in:
2026-01-30 14:25:12 +08:00
commit 8dd8d2668a
899 changed files with 90844 additions and 0 deletions

39
qingyun-service/pom.xml Normal file
View 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>

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -0,0 +1,11 @@
package com.qingyun.service.compare;
import lombok.Data;
@Data
public class BoundingBox {
float x;
float y;
float width;
float height;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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];
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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('.'));
}
}

View File

@@ -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);
}

View File

@@ -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); // 新增:判断是否支持该扩展名
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}
}

View File

@@ -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;
/**
* 源文件差异类型insertupdate, delete
*/
private String baseDiffType;
/**
* 源文件页码
*/
private Integer basePageNum;
/**
* 源文件行号
*/
private Integer baseLineNum;
/**
* 对比文件定位
*/
private String compareBoxArea;
/**
* 对比文件差异内容
*/
private String compareDiffContent;
/**
* 对比文件差异类型insertupdate, delete
*/
private String compareDiffType;
/**
* 对比文件页码
*/
private Integer comparePageNum;
/**
* 对比文件行号
*/
private Integer compareLineNum;
private Date createTime;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
/**
* 源文件差异类型insertupdate, delete
*/
private String baseDiffType;
/**
* 源文件页码
*/
private Integer basePageNum;
/**
* 对比文件定位
*/
private String compareBoxArea;
/**
* 对比文件差异内容
*/
private String compareDiffContent;
/**
* 对比文件差异类型insertupdate, delete
*/
private String compareDiffType;
/**
* 对比文件页码
*/
private Integer comparePageNum;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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; // 评审依据 / 判定理由
}

View File

@@ -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;
}

View File

@@ -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;
/**
* 源文件差异类型insertupdate, delete
*/
private String baseDiffType;
/**
* 源文件页码
*/
private Integer basePageNum;
/**
* 源文件行号
*/
private Integer baseLineNum;
/**
* 对比文件定位
*/
private String compareBoxArea;
/**
* 对比文件差异内容
*/
private String compareDiffContent;
/**
* 对比文件差异类型insertupdate, delete
*/
private String compareDiffType;
/**
* 对比文件页码
*/
private Integer comparePageNum;
/**
* 对比文件行号
*/
private Integer compareLineNum;
private Date createTime;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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('.'));
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>