|
|
@ -4,15 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
|
|
|
import com.fasterxml.jackson.databind.JsonNode; |
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper; |
|
|
|
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension; |
|
|
|
import com.vladsch.flexmark.ext.tables.TablesExtension; |
|
|
|
import com.vladsch.flexmark.html.HtmlRenderer; |
|
|
|
import com.vladsch.flexmark.parser.Parser; |
|
|
|
import com.vladsch.flexmark.util.ast.Node; |
|
|
|
import com.vladsch.flexmark.util.data.MutableDataSet; |
|
|
|
import jakarta.servlet.http.HttpServletResponse; |
|
|
|
import lombok.RequiredArgsConstructor; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.apache.poi.xwpf.usermodel.*; |
|
|
|
import org.dromara.common.core.service.DictService; |
|
|
|
import org.dromara.common.core.utils.StringUtils; |
|
|
|
import org.dromara.productManagement.domain.ContractualTaskResultDetail; |
|
|
@ -24,13 +19,9 @@ import org.dromara.productManagement.service.IContractualTaskResultsService; |
|
|
|
import org.dromara.productManagement.service.IContractualTasksService; |
|
|
|
import org.dromara.system.domain.vo.SysOssVo; |
|
|
|
import org.dromara.system.service.ISysOssService; |
|
|
|
import org.jsoup.Jsoup; |
|
|
|
import org.jsoup.nodes.Document; |
|
|
|
import org.jsoup.safety.Safelist; |
|
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
|
import org.springframework.beans.factory.annotation.Value; |
|
|
|
import org.springframework.stereotype.Service; |
|
|
|
import org.xhtmlrenderer.pdf.ITextRenderer; |
|
|
|
|
|
|
|
import javax.sql.DataSource; |
|
|
|
import java.io.*; |
|
|
@ -39,6 +30,7 @@ import java.nio.charset.StandardCharsets; |
|
|
|
import java.sql.Connection; |
|
|
|
import java.sql.PreparedStatement; |
|
|
|
import java.sql.SQLException; |
|
|
|
import java.util.ArrayList; |
|
|
|
import java.util.Arrays; |
|
|
|
import java.util.List; |
|
|
|
import java.util.zip.ZipEntry; |
|
|
@ -161,7 +153,6 @@ public class ContractualTaskResultsServiceImpl implements IContractualTaskResult |
|
|
|
@Override |
|
|
|
public void downloadResult(Long[] ids, HttpServletResponse response) { |
|
|
|
try { |
|
|
|
// 使用新的导出逻辑,基于getDetailResultsByTaskId获取结果
|
|
|
|
if (ids.length > 0) { |
|
|
|
if (ids.length > 1) { |
|
|
|
// 多文件压缩下载
|
|
|
@ -181,43 +172,22 @@ public class ContractualTaskResultsServiceImpl implements IContractualTaskResult |
|
|
|
|
|
|
|
// 获取详细的任务结果
|
|
|
|
List<ContractualTaskResultVO> resultDetails = getDetailResultsByTaskId(String.valueOf(taskId)); |
|
|
|
if (resultDetails != null && !resultDetails.isEmpty()) { |
|
|
|
// 第一个是"全部"分类
|
|
|
|
ContractualTaskResultVO allCategory = resultDetails.get(0); |
|
|
|
if (allCategory.getResults() != null && !allCategory.getResults().isEmpty()) { |
|
|
|
// 获取任务类型对应的字段配置
|
|
|
|
List<FieldConfig> fieldConfigs = getFieldConfigsByTaskType(taskName); |
|
|
|
|
|
|
|
// 将结果转换为Markdown
|
|
|
|
StringBuilder markdown = new StringBuilder(); |
|
|
|
for (ContractualTaskResultVO.ResultItem item : allCategory.getResults()) { |
|
|
|
markdown.append("## ").append(item.getSerialNumber()).append(". ").append(item.getExistingIssues()).append("\n\n"); |
|
|
|
|
|
|
|
// 根据配置添加各字段内容
|
|
|
|
for (FieldConfig config : fieldConfigs) { |
|
|
|
String fieldValue = getFieldValue(item, config.getField()); |
|
|
|
if (StringUtils.isNotBlank(fieldValue)) { |
|
|
|
markdown.append("### ").append(config.getTitle()).append("\n\n").append(fieldValue).append("\n\n"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 添加分隔线
|
|
|
|
markdown.append("---\n\n"); |
|
|
|
} |
|
|
|
|
|
|
|
// 为每个结果创建PDF文件
|
|
|
|
String pdfFileName = label + "_" + documentName + "_" + ".pdf"; |
|
|
|
String pdfPath = tempDir + File.separator + pdfFileName; |
|
|
|
|
|
|
|
// 转换Markdown为PDF
|
|
|
|
convertMarkdownToPdf(markdown.toString(), pdfPath); |
|
|
|
if (resultDetails != null && resultDetails.size() > 1) { |
|
|
|
// 跳过第一个"全部"分类,从第二个开始处理
|
|
|
|
List<ContractualTaskResultVO> categorizedResults = resultDetails.subList(1, resultDetails.size()); |
|
|
|
|
|
|
|
// 为每个结果创建docx文件
|
|
|
|
String docxFileName = label + "_" + documentName + "_" + ".docx"; |
|
|
|
String docxPath = tempDir + File.separator + docxFileName; |
|
|
|
|
|
|
|
// 生成docx文档
|
|
|
|
generateDocxDocument(categorizedResults, docxPath); |
|
|
|
|
|
|
|
// 添加到ZIP
|
|
|
|
addToZip(pdfPath, pdfFileName, zos); |
|
|
|
addToZip(docxPath, docxFileName, zos); |
|
|
|
|
|
|
|
// 删除临时PDF文件
|
|
|
|
new File(pdfPath).delete(); |
|
|
|
} |
|
|
|
// 删除临时docx文件
|
|
|
|
new File(docxPath).delete(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -242,46 +212,25 @@ public class ContractualTaskResultsServiceImpl implements IContractualTaskResult |
|
|
|
|
|
|
|
// 获取详细的任务结果
|
|
|
|
List<ContractualTaskResultVO> resultDetails = getDetailResultsByTaskId(String.valueOf(taskId)); |
|
|
|
if (resultDetails == null || resultDetails.isEmpty() || |
|
|
|
resultDetails.get(0).getResults() == null || resultDetails.get(0).getResults().isEmpty()) { |
|
|
|
if (resultDetails == null || resultDetails.size() <= 1) { |
|
|
|
throw new RuntimeException("未找到相关结果"); |
|
|
|
} |
|
|
|
|
|
|
|
// 第一个是"全部"分类
|
|
|
|
ContractualTaskResultVO allCategory = resultDetails.get(0); |
|
|
|
|
|
|
|
// 获取任务类型对应的字段配置
|
|
|
|
List<FieldConfig> fieldConfigs = getFieldConfigsByTaskType(taskName); |
|
|
|
|
|
|
|
// 将结果转换为Markdown
|
|
|
|
StringBuilder markdown = new StringBuilder(); |
|
|
|
for (ContractualTaskResultVO.ResultItem item : allCategory.getResults()) { |
|
|
|
markdown.append("## ").append(item.getSerialNumber()).append(". ").append(item.getExistingIssues()).append("\n\n"); |
|
|
|
|
|
|
|
// 根据配置添加各字段内容
|
|
|
|
for (FieldConfig config : fieldConfigs) { |
|
|
|
String fieldValue = getFieldValue(item, config.getField()); |
|
|
|
if (StringUtils.isNotBlank(fieldValue)) { |
|
|
|
markdown.append("### ").append(config.getTitle()).append("\n\n").append(fieldValue).append("\n\n"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 添加分隔线
|
|
|
|
markdown.append("---\n\n"); |
|
|
|
} |
|
|
|
// 跳过第一个"全部"分类,从第二个开始处理
|
|
|
|
List<ContractualTaskResultVO> categorizedResults = resultDetails.subList(1, resultDetails.size()); |
|
|
|
|
|
|
|
String pdfFileName = label + "_" + documentName + "_" + ".pdf"; |
|
|
|
String docxFileName = label + "_" + documentName + "_" + ".docx"; |
|
|
|
String tempDir = System.getProperty("java.io.tmpdir"); |
|
|
|
String pdfPath = tempDir + File.separator + pdfFileName; |
|
|
|
String docxPath = tempDir + File.separator + docxFileName; |
|
|
|
|
|
|
|
// 转换Markdown为PDF
|
|
|
|
convertMarkdownToPdf(markdown.toString(), pdfPath); |
|
|
|
// 生成docx文档
|
|
|
|
generateDocxDocument(categorizedResults, docxPath); |
|
|
|
|
|
|
|
// 下载PDF文件
|
|
|
|
downloadFile(response, pdfPath, pdfFileName); |
|
|
|
// 下载docx文件
|
|
|
|
downloadFile(response, docxPath, docxFileName); |
|
|
|
|
|
|
|
// 删除临时PDF文件
|
|
|
|
new File(pdfPath).delete(); |
|
|
|
// 删除临时docx文件
|
|
|
|
new File(docxPath).delete(); |
|
|
|
} |
|
|
|
} else { |
|
|
|
throw new RuntimeException("未找到相关文件"); |
|
|
@ -329,7 +278,7 @@ public class ContractualTaskResultsServiceImpl implements IContractualTaskResult |
|
|
|
previewDir.mkdirs(); |
|
|
|
} |
|
|
|
|
|
|
|
filerotPath = fileRootPath + fileInfo.getFileName(); |
|
|
|
filerotPath = fileRootPath +File.separator+ fileInfo.getFileName(); |
|
|
|
|
|
|
|
// 如果不是pdf文件,需要转换
|
|
|
|
if (!fileName.toLowerCase().endsWith(".pdf")) { |
|
|
@ -392,86 +341,133 @@ public class ContractualTaskResultsServiceImpl implements IContractualTaskResult |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Markdown转PDF方法
|
|
|
|
private void convertMarkdownToPdf(String markdown, String outputPath) throws Exception { |
|
|
|
MutableDataSet options = new MutableDataSet(); |
|
|
|
// 启用表格解析
|
|
|
|
options.set(Parser.EXTENSIONS, Arrays.asList( |
|
|
|
TablesExtension.create(), |
|
|
|
StrikethroughExtension.create() |
|
|
|
)); |
|
|
|
|
|
|
|
Parser parser = Parser.builder(options).build(); |
|
|
|
HtmlRenderer renderer = HtmlRenderer.builder(options) |
|
|
|
.extensions(Arrays.asList( |
|
|
|
TablesExtension.create(), |
|
|
|
StrikethroughExtension.create() |
|
|
|
)) |
|
|
|
.build(); |
|
|
|
|
|
|
|
Node document = parser.parse(markdown); |
|
|
|
String htmlContent = renderer.render(document); |
|
|
|
// 使用 JSoup 清理
|
|
|
|
Document.OutputSettings settings = new Document.OutputSettings(); |
|
|
|
settings.prettyPrint(false); |
|
|
|
|
|
|
|
// 配置允许的标签
|
|
|
|
Safelist safelist = Safelist.relaxed() |
|
|
|
.addTags("em", "strong", "div", "span", "br", "table", "thead", "tbody", "tr", "th", "td", "img") |
|
|
|
.addAttributes(":all", "style", "class") |
|
|
|
.addAttributes("table", "border", "cellpadding", "cellspacing") |
|
|
|
.addAttributes("img", "src", "alt", "width", "height"); // 允许img标签及其属性
|
|
|
|
htmlContent = Jsoup.clean(htmlContent, "", safelist, settings); |
|
|
|
|
|
|
|
// 创建完整的HTML文档,使用微软雅黑
|
|
|
|
String html = String.format(""" |
|
|
|
<!DOCTYPE html> |
|
|
|
<html> |
|
|
|
<head> |
|
|
|
<meta charset="UTF-8"/> |
|
|
|
<style type="text/css"> |
|
|
|
body { |
|
|
|
font-family: Microsoft YaHei; |
|
|
|
line-height: 1.6; |
|
|
|
margin: 20px; |
|
|
|
font-size: 14px; |
|
|
|
} |
|
|
|
p { margin: 10px 0; } |
|
|
|
pre { |
|
|
|
background-color: #f5f5f5; |
|
|
|
padding: 10px; |
|
|
|
border-radius: 4px; |
|
|
|
} |
|
|
|
/* 增强表格样式 */ |
|
|
|
table { |
|
|
|
border-collapse: collapse; |
|
|
|
width: 100%%; |
|
|
|
margin: 10px 0; |
|
|
|
table-layout: fixed; |
|
|
|
} |
|
|
|
th, td { |
|
|
|
border: 1px solid #ddd; |
|
|
|
padding: 8px; |
|
|
|
text-align: left; |
|
|
|
word-wrap: break-word; |
|
|
|
} |
|
|
|
th { |
|
|
|
background-color: #f2f2f2; |
|
|
|
font-weight: bold; |
|
|
|
} |
|
|
|
</style> |
|
|
|
</head> |
|
|
|
<body> |
|
|
|
%s |
|
|
|
</body> |
|
|
|
</html> |
|
|
|
""", htmlContent); |
|
|
|
|
|
|
|
try (OutputStream outputStream = new FileOutputStream(outputPath)) { |
|
|
|
ITextRenderer renderer2 = new ITextRenderer(); |
|
|
|
renderer2.setDocumentFromString(html); |
|
|
|
renderer2.layout(); |
|
|
|
renderer2.createPDF(outputStream); |
|
|
|
@Override |
|
|
|
public void getBidPdfStream(String taskId, HttpServletResponse response) { |
|
|
|
try { |
|
|
|
// 查询任务信息
|
|
|
|
ContractualTasksVo task = contractualTasksService.queryById(Long.valueOf(taskId)); |
|
|
|
if (task == null) { |
|
|
|
throw new RuntimeException("任务不存在"); |
|
|
|
} |
|
|
|
|
|
|
|
String outputPath = tempfilePath + File.separator + "pdf_preview" + File.separator; |
|
|
|
String bidPath = task.getBidPath(); |
|
|
|
String filePath = ""; |
|
|
|
String filerotPath = ""; |
|
|
|
|
|
|
|
if (StringUtils.isNotEmpty(bidPath)) { |
|
|
|
// 如果bidPath存在,直接使用该路径
|
|
|
|
filePath = bidPath; |
|
|
|
} else { |
|
|
|
// 如果bidPath不存在,从reviewData中解析bidDocumentPath
|
|
|
|
String reviewData = task.getReviewData(); |
|
|
|
if (StringUtils.isEmpty(reviewData)) { |
|
|
|
throw new RuntimeException("未找到审查数据"); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
ObjectMapper objectMapper = new ObjectMapper(); |
|
|
|
JsonNode rootNode = objectMapper.readTree(reviewData); |
|
|
|
JsonNode consistencyNode = rootNode.get("consistency"); |
|
|
|
|
|
|
|
if (consistencyNode == null || consistencyNode.isNull()) { |
|
|
|
throw new RuntimeException("未找到一致性审查配置"); |
|
|
|
} |
|
|
|
|
|
|
|
JsonNode bidDocumentPathNode = consistencyNode.get("bidDocumentPath"); |
|
|
|
if (bidDocumentPathNode == null || bidDocumentPathNode.isNull()) { |
|
|
|
throw new RuntimeException("未找到招投标文件路径"); |
|
|
|
} |
|
|
|
|
|
|
|
String bidDocumentPath = bidDocumentPathNode.asText(); |
|
|
|
if (StringUtils.isEmpty(bidDocumentPath)) { |
|
|
|
throw new RuntimeException("招投标文件路径为空"); |
|
|
|
} |
|
|
|
|
|
|
|
log.info("从reviewData中获取到招投标文件路径: " + bidDocumentPath); |
|
|
|
|
|
|
|
// 创建pdf预览目录
|
|
|
|
File previewDir = new File(outputPath); |
|
|
|
if (!previewDir.exists()) { |
|
|
|
previewDir.mkdirs(); |
|
|
|
} |
|
|
|
|
|
|
|
filerotPath = bidDocumentPath; |
|
|
|
|
|
|
|
// 检查文件是否为PDF格式
|
|
|
|
if (!bidDocumentPath.toLowerCase().endsWith(".pdf")) { |
|
|
|
// 需要转换为PDF
|
|
|
|
String fileName = new File(bidDocumentPath).getName(); |
|
|
|
String fileNameWithoutExt = fileName.substring(0, fileName.lastIndexOf(".")); |
|
|
|
filePath = outputPath + fileNameWithoutExt + ".pdf"; |
|
|
|
log.info("需要转换招投标文件: " + bidDocumentPath + " -> " + filePath); |
|
|
|
} else { |
|
|
|
// 已经是PDF格式,直接使用
|
|
|
|
filePath = bidDocumentPath; |
|
|
|
} |
|
|
|
|
|
|
|
} catch (Exception e) { |
|
|
|
log.error("解析reviewData失败", e); |
|
|
|
throw new RuntimeException("解析审查数据失败: " + e.getMessage()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
log.info("开始输出招投标文件: " + filePath); |
|
|
|
|
|
|
|
// 读取文件并输出到response
|
|
|
|
File file = new File(filePath); |
|
|
|
if (!file.exists()) { |
|
|
|
log.info("招投标文件不存在,开始转换: " + filePath); |
|
|
|
|
|
|
|
// 使用libreoffice转换为pdf
|
|
|
|
ProcessBuilder builder = new ProcessBuilder("libreoffice", "--headless", "--convert-to", "pdf", filerotPath, "--outdir", outputPath); |
|
|
|
Process process = builder.start(); |
|
|
|
int completed = process.waitFor(); |
|
|
|
if (completed != 0) { |
|
|
|
InputStream errorStream = process.getErrorStream(); |
|
|
|
String errorMessage = new String(errorStream.readAllBytes()); |
|
|
|
process.destroyForcibly(); |
|
|
|
throw new RuntimeException("招投标文件转换失败: " + errorMessage); |
|
|
|
} |
|
|
|
process.destroyForcibly(); |
|
|
|
|
|
|
|
// 重新检查转换后的文件
|
|
|
|
if (!file.exists()) { |
|
|
|
throw new RuntimeException("招投标文件转换后仍不存在: " + filePath); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
log.info("招投标文件存在,开始输出: " + filePath); |
|
|
|
|
|
|
|
// 使用JDBC更新数据库中的bid_path
|
|
|
|
try (Connection conn = dataSource.getConnection()) { |
|
|
|
String sql = "UPDATE contractual_tasks SET bid_path = ? WHERE id = ?"; |
|
|
|
try (PreparedStatement pstmt = conn.prepareStatement(sql)) { |
|
|
|
pstmt.setString(1, filePath); |
|
|
|
pstmt.setLong(2, Long.parseLong(taskId)); |
|
|
|
pstmt.executeUpdate(); |
|
|
|
} |
|
|
|
} catch (SQLException e) { |
|
|
|
log.error("更新招投标PDF路径失败", e); |
|
|
|
throw new RuntimeException("更新招投标PDF路径失败: " + e.getMessage()); |
|
|
|
} |
|
|
|
|
|
|
|
response.setContentType("application/pdf"); |
|
|
|
response.setHeader("Content-Disposition", "inline; filename=" + URLEncoder.encode(file.getName(), StandardCharsets.UTF_8.name())); |
|
|
|
|
|
|
|
try (FileInputStream fis = new FileInputStream(file); |
|
|
|
BufferedInputStream bis = new BufferedInputStream(fis, 8192); |
|
|
|
OutputStream os = response.getOutputStream()) { |
|
|
|
byte[] buffer = new byte[8192]; |
|
|
|
int bytesRead; |
|
|
|
while ((bytesRead = bis.read(buffer)) != -1) { |
|
|
|
os.write(buffer, 0, bytesRead); |
|
|
|
} |
|
|
|
os.flush(); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("获取招投标PDF文件流失败", e); |
|
|
|
throw new RuntimeException("获取招投标PDF文件流失败: " + e.getMessage()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -519,40 +515,251 @@ public class ContractualTaskResultsServiceImpl implements IContractualTaskResult |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static class FieldConfig { |
|
|
|
private String field; |
|
|
|
private String title; |
|
|
|
/** |
|
|
|
* 生成docx文档 |
|
|
|
* |
|
|
|
* @param categorizedResults 分类结果列表 |
|
|
|
* @param outputPath 输出路径 |
|
|
|
* @throws Exception 异常 |
|
|
|
*/ |
|
|
|
private void generateDocxDocument(List<ContractualTaskResultVO> categorizedResults, String outputPath) throws Exception { |
|
|
|
try (XWPFDocument document = new XWPFDocument()) { |
|
|
|
int categoryIndex = 1; // 一级标题序号
|
|
|
|
|
|
|
|
for (ContractualTaskResultVO category : categorizedResults) { |
|
|
|
if (category.getResults() == null || category.getResults().isEmpty()) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
// 添加一级标题(带序号)
|
|
|
|
XWPFParagraph title = document.createParagraph(); |
|
|
|
XWPFRun titleRun = title.createRun(); |
|
|
|
titleRun.setText(categoryIndex + ". " + category.getName()); |
|
|
|
titleRun.setBold(true); |
|
|
|
titleRun.setFontSize(16); |
|
|
|
titleRun.setFontFamily("Microsoft YaHei"); |
|
|
|
|
|
|
|
// 根据分类名称获取表格配置
|
|
|
|
TableConfiguration tableConfig = getTableConfiguration(category.getName()); |
|
|
|
|
|
|
|
int issueIndex = 1; // 二级标题序号
|
|
|
|
for (ContractualTaskResultVO.ResultItem item : category.getResults()) { |
|
|
|
// 添加二级标题 - 问题点(带序号)
|
|
|
|
XWPFParagraph issueTitle = document.createParagraph(); |
|
|
|
XWPFRun issueTitleRun = issueTitle.createRun(); |
|
|
|
issueTitleRun.setText(categoryIndex + "." + issueIndex + ". 问题点:" + item.getExistingIssues()); |
|
|
|
issueTitleRun.setBold(true); |
|
|
|
issueTitleRun.setFontSize(12); |
|
|
|
issueTitleRun.setFontFamily("Microsoft YaHei"); |
|
|
|
|
|
|
|
// 创建表格
|
|
|
|
createTableForItem(document, tableConfig, item, category.getName()); |
|
|
|
|
|
|
|
// 添加段落间距
|
|
|
|
XWPFParagraph spacer = document.createParagraph(); |
|
|
|
spacer.createRun().addBreak(); |
|
|
|
|
|
|
|
issueIndex++; |
|
|
|
} |
|
|
|
|
|
|
|
categoryIndex++; |
|
|
|
} |
|
|
|
|
|
|
|
// 写入文件
|
|
|
|
try (FileOutputStream out = new FileOutputStream(outputPath)) { |
|
|
|
document.write(out); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 为特定项目创建表格 - 使用新的坐标配置系统 |
|
|
|
*/ |
|
|
|
private void createTableForItem(XWPFDocument document, TableConfiguration config, ContractualTaskResultVO.ResultItem item, String categoryName) { |
|
|
|
XWPFTable table = document.createTable(); |
|
|
|
setTableStyle(table); |
|
|
|
|
|
|
|
public FieldConfig(String field, String title) { |
|
|
|
this.field = field; |
|
|
|
this.title = title; |
|
|
|
// 创建表格的所有行
|
|
|
|
for (int i = 1; i < config.getTotalRows(); i++) { |
|
|
|
table.createRow(); |
|
|
|
} |
|
|
|
|
|
|
|
public String getField() { |
|
|
|
return field; |
|
|
|
// 确保表格有足够的列
|
|
|
|
for (int i = 0; i < config.getTotalRows(); i++) { |
|
|
|
XWPFTableRow row = table.getRow(i); |
|
|
|
while (row.getTableCells().size() < config.getTotalColumns()) { |
|
|
|
row.addNewTableCell(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public String getTitle() { |
|
|
|
return title; |
|
|
|
// 根据配置填充单元格
|
|
|
|
for (CellConfiguration cellConfig : config.getCellConfigs()) { |
|
|
|
XWPFTableRow row = table.getRow(cellConfig.getRow()); |
|
|
|
XWPFTableCell cell = row.getCell(cellConfig.getColumn()); |
|
|
|
|
|
|
|
String content = ""; |
|
|
|
if (cellConfig.isHeader()) { |
|
|
|
content = cellConfig.getHeaderText(); |
|
|
|
} else { |
|
|
|
content = getFieldValue(item, cellConfig.getFieldName()); |
|
|
|
} |
|
|
|
|
|
|
|
setupTableCell(cell, content, cellConfig.isHeader()); |
|
|
|
|
|
|
|
// 处理合并
|
|
|
|
if (cellConfig.getMergeType() != MergeType.NONE) { |
|
|
|
applyCellMerge(row, cellConfig); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 根据任务类型获取字段配置 |
|
|
|
* |
|
|
|
* @param taskName 任务名称 |
|
|
|
* @return 字段配置列表 |
|
|
|
* 应用单元格合并 |
|
|
|
*/ |
|
|
|
private void applyCellMerge(XWPFTableRow row, CellConfiguration cellConfig) { |
|
|
|
switch (cellConfig.getMergeType()) { |
|
|
|
case HORIZONTAL: |
|
|
|
int endCol = cellConfig.getColumn() + cellConfig.getMergeSpanColumns() - 1; |
|
|
|
mergeCellsHorizontally(row, cellConfig.getColumn(), endCol); |
|
|
|
break; |
|
|
|
case VERTICAL: |
|
|
|
// 垂直合并的实现(如果需要的话)
|
|
|
|
// mergeVertically(table, cellConfig.getRow(), cellConfig.getColumn(), cellConfig.getMergeSpanRows());
|
|
|
|
break; |
|
|
|
case BOTH: |
|
|
|
// 双向合并的实现(如果需要的话)
|
|
|
|
break; |
|
|
|
default: |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 格式化审查依据字段 |
|
|
|
*/ |
|
|
|
private String formatReviewBasisField(String reviewBasis, String fieldName) { |
|
|
|
if (StringUtils.isBlank(reviewBasis)) { |
|
|
|
return ""; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
ObjectMapper objectMapper = new ObjectMapper(); |
|
|
|
JsonNode jsonNode = objectMapper.readTree(reviewBasis); |
|
|
|
|
|
|
|
if (fieldName.equals("review_points[0]")) { |
|
|
|
if (jsonNode.has("review_points") && jsonNode.get("review_points").isArray()) { |
|
|
|
JsonNode arrayNode = jsonNode.get("review_points"); |
|
|
|
if (arrayNode.size() > 0) { |
|
|
|
return arrayNode.get(0).asText(); |
|
|
|
} |
|
|
|
} |
|
|
|
} else if (fieldName.equals("review_content")) { |
|
|
|
if (jsonNode.has("review_content") && !jsonNode.get("review_content").isNull()) { |
|
|
|
return jsonNode.get("review_content").asText(); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("解析审查依据字段失败: " + fieldName, e); |
|
|
|
} |
|
|
|
|
|
|
|
return ""; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 设置表格单元格 - 简化版 |
|
|
|
*/ |
|
|
|
private void setupTableCell(XWPFTableCell cell, String text, boolean isHeader) { |
|
|
|
// 清除默认内容
|
|
|
|
if (cell.getParagraphs().size() > 0) { |
|
|
|
cell.removeParagraph(0); |
|
|
|
} |
|
|
|
|
|
|
|
XWPFParagraph paragraph = cell.addParagraph(); |
|
|
|
XWPFRun run = paragraph.createRun(); |
|
|
|
run.setText(text != null ? text : ""); |
|
|
|
run.setFontFamily("Microsoft YaHei"); |
|
|
|
run.setFontSize(12); |
|
|
|
|
|
|
|
if (isHeader) { |
|
|
|
run.setBold(true); |
|
|
|
// 设置表头背景色
|
|
|
|
cell.setColor("E6E6E6"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 设置表格样式 - 简化版 |
|
|
|
*/ |
|
|
|
private void setTableStyle(XWPFTable table) { |
|
|
|
// 设置表格宽度为100%
|
|
|
|
table.setWidth("100%"); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 根据分类名称获取表格配置 |
|
|
|
*/ |
|
|
|
private List<FieldConfig> getFieldConfigsByTaskType(String taskName) { |
|
|
|
// 这里可以根据不同的任务类型返回不同的字段配置
|
|
|
|
// 目前返回通用的配置
|
|
|
|
return Arrays.asList( |
|
|
|
new FieldConfig("originalText", "原文"), |
|
|
|
new FieldConfig("comparedText", "比对原文"), |
|
|
|
new FieldConfig("modifiedContent", "修改后的内容"), |
|
|
|
new FieldConfig("modificationDisplay", "展示修改情况"), |
|
|
|
new FieldConfig("reviewBasis", "审查依据") |
|
|
|
); |
|
|
|
private TableConfiguration getTableConfiguration(String categoryName) { |
|
|
|
TableConfiguration config; |
|
|
|
|
|
|
|
switch (categoryName) { |
|
|
|
case "实质性审查": |
|
|
|
// 4行2列:表头行 + 数据行 + 审查依据标签行 + 审查依据内容行
|
|
|
|
config = new TableConfiguration(categoryName, 4, 2); |
|
|
|
// 表头行
|
|
|
|
config.addCellConfig(new CellConfiguration(0, 0).header("原文")); |
|
|
|
config.addCellConfig(new CellConfiguration(0, 1).header("修改建议")); |
|
|
|
// 数据行
|
|
|
|
config.addCellConfig(new CellConfiguration(1, 0).field("originalText")); |
|
|
|
config.addCellConfig(new CellConfiguration(1, 1).field("modifiedContent")); |
|
|
|
// 审查依据标签行(跨两列合并)
|
|
|
|
config.addCellConfig(new CellConfiguration(2, 0).header("审查依据").mergeHorizontal(2)); |
|
|
|
// 审查依据内容行(跨两列合并)
|
|
|
|
config.addCellConfig(new CellConfiguration(3, 0).field("review_basis_combined").mergeHorizontal(2)); |
|
|
|
break; |
|
|
|
|
|
|
|
case "合规性审查": |
|
|
|
// 4行2列:表头行 + 数据行 + 法规依据标签行 + 法规依据内容行
|
|
|
|
config = new TableConfiguration(categoryName, 4, 1); |
|
|
|
// 表头行
|
|
|
|
config.addCellConfig(new CellConfiguration(0, 0).header("原文")); |
|
|
|
// config.addCellConfig(new CellConfiguration(0, 1).header("修改后的内容"));
|
|
|
|
// 数据行
|
|
|
|
config.addCellConfig(new CellConfiguration(1, 0).field("originalText")); |
|
|
|
// config.addCellConfig(new CellConfiguration(1, 1).field("modifiedContent"));
|
|
|
|
// 法规依据标签行(跨两列合并)
|
|
|
|
config.addCellConfig(new CellConfiguration(2, 0).header("法规依据").mergeHorizontal(2)); |
|
|
|
// 法规依据内容行(跨两列合并)
|
|
|
|
config.addCellConfig(new CellConfiguration(3, 0).field("review_points[0]").mergeHorizontal(2)); |
|
|
|
break; |
|
|
|
|
|
|
|
case "一致性审查": |
|
|
|
// 4行3列:表头行 + 数据行 + 修改后内容标签行 + 修改后内容数据行
|
|
|
|
config = new TableConfiguration(categoryName, 4, 2); |
|
|
|
// 表头行
|
|
|
|
config.addCellConfig(new CellConfiguration(0, 0).header("合同原文")); |
|
|
|
config.addCellConfig(new CellConfiguration(0, 1).header("招标文件原文")); |
|
|
|
// 数据行
|
|
|
|
config.addCellConfig(new CellConfiguration(1, 0).field("originalText")); |
|
|
|
config.addCellConfig(new CellConfiguration(1, 1).field("comparedText")); |
|
|
|
// 修改后内容标签行(跨三列合并)
|
|
|
|
config.addCellConfig(new CellConfiguration(2, 0).header("问题描述").mergeHorizontal(2)); |
|
|
|
// 修改后内容数据行(跨三列合并)
|
|
|
|
config.addCellConfig(new CellConfiguration(3, 0).field("existingIssues").mergeHorizontal(2)); |
|
|
|
break; |
|
|
|
|
|
|
|
default: |
|
|
|
// 默认配置:4行2列
|
|
|
|
config = new TableConfiguration(categoryName, 4, 2); |
|
|
|
config.addCellConfig(new CellConfiguration(0, 0).header("原文")); |
|
|
|
config.addCellConfig(new CellConfiguration(0, 1).header("修改建议")); |
|
|
|
config.addCellConfig(new CellConfiguration(1, 0).field("originalText")); |
|
|
|
config.addCellConfig(new CellConfiguration(1, 1).field("modifiedContent")); |
|
|
|
config.addCellConfig(new CellConfiguration(2, 0).header("审查依据").mergeHorizontal(2)); |
|
|
|
config.addCellConfig(new CellConfiguration(3, 0).field("review_basis_combined").mergeHorizontal(2)); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
return config; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
@ -563,6 +770,10 @@ public class ContractualTaskResultsServiceImpl implements IContractualTaskResult |
|
|
|
* @return 字段值 |
|
|
|
*/ |
|
|
|
private String getFieldValue(ContractualTaskResultVO.ResultItem item, String fieldName) { |
|
|
|
if (fieldName == null || fieldName.isEmpty()) { |
|
|
|
return ""; |
|
|
|
} |
|
|
|
|
|
|
|
switch (fieldName) { |
|
|
|
case "originalText": |
|
|
|
return item.getOriginalText(); |
|
|
@ -573,36 +784,235 @@ public class ContractualTaskResultsServiceImpl implements IContractualTaskResult |
|
|
|
case "modificationDisplay": |
|
|
|
return item.getModificationDisplay(); |
|
|
|
case "reviewBasis": |
|
|
|
if (item.getReviewBasis() != null && StringUtils.isNotBlank(item.getReviewBasis())) { |
|
|
|
// reviewBasis现在是JSON字符串,可以直接返回
|
|
|
|
// 或者解析后格式化显示
|
|
|
|
return item.getReviewBasis(); |
|
|
|
case "existingIssues": |
|
|
|
return item.getExistingIssues(); |
|
|
|
case "review_basis_combined": |
|
|
|
// 组合审查依据:review_points[0] + review_content
|
|
|
|
return formatReviewBasisCombined(item.getReviewBasis()); |
|
|
|
case "review_points[0]": |
|
|
|
return formatReviewBasisField(item.getReviewBasis(), "review_points[0]"); |
|
|
|
case "review_content": |
|
|
|
return formatReviewBasisField(item.getReviewBasis(), "review_content"); |
|
|
|
case "": |
|
|
|
return ""; |
|
|
|
default: |
|
|
|
return ""; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 格式化组合的审查依据 |
|
|
|
*/ |
|
|
|
private String formatReviewBasisCombined(String reviewBasis) { |
|
|
|
if (StringUtils.isBlank(reviewBasis)) { |
|
|
|
return ""; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
ObjectMapper objectMapper = new ObjectMapper(); |
|
|
|
JsonNode jsonNode = objectMapper.readTree(item.getReviewBasis()); |
|
|
|
JsonNode jsonNode = objectMapper.readTree(reviewBasis); |
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder(); |
|
|
|
if (jsonNode.has("reviewContent") && !jsonNode.get("reviewContent").isNull()) { |
|
|
|
sb.append("审查内容: ").append(jsonNode.get("reviewContent").asText()).append("\n"); |
|
|
|
} |
|
|
|
if (jsonNode.has("reviewPoints") && jsonNode.get("reviewPoints").isArray()) { |
|
|
|
sb.append("审查点: "); |
|
|
|
for (JsonNode point : jsonNode.get("reviewPoints")) { |
|
|
|
sb.append(point.asText()).append(", "); |
|
|
|
} |
|
|
|
// 移除最后的逗号和空格
|
|
|
|
if (sb.length() > 4) { |
|
|
|
sb.setLength(sb.length() - 2); |
|
|
|
} |
|
|
|
} |
|
|
|
return sb.toString(); |
|
|
|
StringBuilder result = new StringBuilder(); |
|
|
|
|
|
|
|
// 添加review_points[0]
|
|
|
|
if (jsonNode.has("review_points") && jsonNode.get("review_points").isArray()) { |
|
|
|
JsonNode arrayNode = jsonNode.get("review_points"); |
|
|
|
if (arrayNode.size() > 0) { |
|
|
|
result.append(arrayNode.get(0).asText()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 添加review_content
|
|
|
|
if (jsonNode.has("review_content") && !jsonNode.get("review_content").isNull()) { |
|
|
|
String content = jsonNode.get("review_content").asText(); |
|
|
|
if (StringUtils.isNotBlank(content)) { |
|
|
|
if (result.length() > 0) { |
|
|
|
result.append(" "); |
|
|
|
} |
|
|
|
result.append(content); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return result.toString(); |
|
|
|
} catch (Exception e) { |
|
|
|
// 如果JSON解析失败,直接返回原始字符串
|
|
|
|
return item.getReviewBasis(); |
|
|
|
log.warn("解析组合审查依据失败", e); |
|
|
|
return reviewBasis; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 水平合并单元格 - 使用POI真正合并功能 |
|
|
|
* @param row 表格行 |
|
|
|
* @param fromCol 起始列 |
|
|
|
* @param toCol 结束列 |
|
|
|
*/ |
|
|
|
private void mergeCellsHorizontally(XWPFTableRow row, int fromCol, int toCol) { |
|
|
|
if (fromCol >= toCol) return; |
|
|
|
|
|
|
|
try { |
|
|
|
// 使用POI的真正合并功能
|
|
|
|
for (int i = fromCol; i <= toCol; i++) { |
|
|
|
XWPFTableCell cell = row.getCell(i); |
|
|
|
if (cell != null) { |
|
|
|
if (i == fromCol) { |
|
|
|
// 第一个单元格设置为合并开始
|
|
|
|
cell.getCTTc().addNewTcPr().addNewHMerge().setVal( |
|
|
|
org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.RESTART); |
|
|
|
} else { |
|
|
|
// 其他单元格设置为合并继续
|
|
|
|
cell.getCTTc().addNewTcPr().addNewHMerge().setVal( |
|
|
|
org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.CONTINUE); |
|
|
|
// 清空被合并单元格的内容
|
|
|
|
while (cell.getParagraphs().size() > 0) { |
|
|
|
cell.removeParagraph(0); |
|
|
|
} |
|
|
|
cell.addParagraph(); |
|
|
|
} |
|
|
|
} |
|
|
|
return null; |
|
|
|
default: |
|
|
|
return null; |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("合并单元格失败: fromCol=" + fromCol + ", toCol=" + toCol, e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private boolean isEmpty(String str) { |
|
|
|
return StringUtils.isBlank(str); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 表格配置类 - 基于坐标的配置系统 |
|
|
|
*/ |
|
|
|
private static class TableConfiguration { |
|
|
|
private String categoryName; |
|
|
|
private int totalRows; |
|
|
|
private int totalColumns; |
|
|
|
private List<CellConfiguration> cellConfigs; |
|
|
|
|
|
|
|
public TableConfiguration(String categoryName, int totalRows, int totalColumns) { |
|
|
|
this.categoryName = categoryName; |
|
|
|
this.totalRows = totalRows; |
|
|
|
this.totalColumns = totalColumns; |
|
|
|
this.cellConfigs = new ArrayList<>(); |
|
|
|
} |
|
|
|
|
|
|
|
public void addCellConfig(CellConfiguration cellConfig) { |
|
|
|
this.cellConfigs.add(cellConfig); |
|
|
|
} |
|
|
|
|
|
|
|
// Getters
|
|
|
|
public String getCategoryName() { return categoryName; } |
|
|
|
public int getTotalRows() { return totalRows; } |
|
|
|
public int getTotalColumns() { return totalColumns; } |
|
|
|
public List<CellConfiguration> getCellConfigs() { return cellConfigs; } |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 单元格配置类 |
|
|
|
*/ |
|
|
|
private static class CellConfiguration { |
|
|
|
private int row; |
|
|
|
private int column; |
|
|
|
private String fieldName; // 对应ResultItem的字段名
|
|
|
|
private String headerText; // 表头文本
|
|
|
|
private boolean isHeader; // 是否为表头
|
|
|
|
private MergeType mergeType; // 合并类型
|
|
|
|
private int mergeSpanRows; // 合并的行数
|
|
|
|
private int mergeSpanColumns; // 合并的列数
|
|
|
|
|
|
|
|
public CellConfiguration(int row, int column) { |
|
|
|
this.row = row; |
|
|
|
this.column = column; |
|
|
|
this.mergeType = MergeType.NONE; |
|
|
|
this.mergeSpanRows = 1; |
|
|
|
this.mergeSpanColumns = 1; |
|
|
|
} |
|
|
|
|
|
|
|
// Builder模式的方法
|
|
|
|
public CellConfiguration field(String fieldName) { |
|
|
|
this.fieldName = fieldName; |
|
|
|
return this; |
|
|
|
} |
|
|
|
|
|
|
|
public CellConfiguration header(String headerText) { |
|
|
|
this.headerText = headerText; |
|
|
|
this.isHeader = true; |
|
|
|
return this; |
|
|
|
} |
|
|
|
|
|
|
|
public CellConfiguration mergeHorizontal(int spanColumns) { |
|
|
|
this.mergeType = MergeType.HORIZONTAL; |
|
|
|
this.mergeSpanColumns = spanColumns; |
|
|
|
return this; |
|
|
|
} |
|
|
|
|
|
|
|
public CellConfiguration mergeVertical(int spanRows) { |
|
|
|
this.mergeType = MergeType.VERTICAL; |
|
|
|
this.mergeSpanRows = spanRows; |
|
|
|
return this; |
|
|
|
} |
|
|
|
|
|
|
|
public CellConfiguration mergeBoth(int spanRows, int spanColumns) { |
|
|
|
this.mergeType = MergeType.BOTH; |
|
|
|
this.mergeSpanRows = spanRows; |
|
|
|
this.mergeSpanColumns = spanColumns; |
|
|
|
return this; |
|
|
|
} |
|
|
|
|
|
|
|
// Getters
|
|
|
|
public int getRow() { return row; } |
|
|
|
public int getColumn() { return column; } |
|
|
|
public String getFieldName() { return fieldName; } |
|
|
|
public String getHeaderText() { return headerText; } |
|
|
|
public boolean isHeader() { return isHeader; } |
|
|
|
public MergeType getMergeType() { return mergeType; } |
|
|
|
public int getMergeSpanRows() { return mergeSpanRows; } |
|
|
|
public int getMergeSpanColumns() { return mergeSpanColumns; } |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 合并类型枚举 |
|
|
|
*/ |
|
|
|
private enum MergeType { |
|
|
|
NONE, // 不合并
|
|
|
|
HORIZONTAL, // 水平合并
|
|
|
|
VERTICAL, // 垂直合并
|
|
|
|
BOTH // 双向合并
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 审查依据配置类 - 简化版 |
|
|
|
*/ |
|
|
|
private static class BasisConfiguration { |
|
|
|
private String label; |
|
|
|
private List<FieldCombination> fieldCombinations; |
|
|
|
|
|
|
|
public BasisConfiguration(String label, List<FieldCombination> fieldCombinations) { |
|
|
|
this.label = label; |
|
|
|
this.fieldCombinations = fieldCombinations; |
|
|
|
} |
|
|
|
|
|
|
|
public String getLabel() { return label; } |
|
|
|
public List<FieldCombination> getFieldCombinations() { return fieldCombinations; } |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 字段组合配置类 |
|
|
|
*/ |
|
|
|
private static class FieldCombination { |
|
|
|
private String fieldPath; |
|
|
|
private String separator; |
|
|
|
|
|
|
|
public FieldCombination(String fieldPath, String separator) { |
|
|
|
this.fieldPath = fieldPath; |
|
|
|
this.separator = separator; |
|
|
|
} |
|
|
|
|
|
|
|
public FieldCombination(String fieldPath) { |
|
|
|
this(fieldPath, ""); |
|
|
|
} |
|
|
|
|
|
|
|
public String getFieldPath() { return fieldPath; } |
|
|
|
public String getSeparator() { return separator; } |
|
|
|
} |
|
|
|
} |
|
|
|