diff --git a/src/main/java/com/gxwebsoft/ai/controller/AuditReportController.java b/src/main/java/com/gxwebsoft/ai/controller/AuditReportController.java index 2415b25..e6dba4b 100644 --- a/src/main/java/com/gxwebsoft/ai/controller/AuditReportController.java +++ b/src/main/java/com/gxwebsoft/ai/controller/AuditReportController.java @@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.poi.openxml4j.util.ZipSecureFile; import org.apache.poi.xwpf.usermodel.BreakType; import org.apache.poi.xwpf.usermodel.ParagraphAlignment; +import org.apache.poi.xwpf.usermodel.UnderlinePatterns; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFRun; @@ -35,6 +36,7 @@ import org.apache.poi.xwpf.usermodel.XWPFTableRow; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -47,6 +49,7 @@ import com.gxwebsoft.ai.config.TemplateConfig; import com.gxwebsoft.ai.dto.AuditReportRequest; import com.gxwebsoft.ai.dto.AuditReportSaveRequest; import com.gxwebsoft.ai.dto.KnowledgeBaseRequest; +import com.gxwebsoft.ai.dto.AuditReportDefaultTextRequest; import com.gxwebsoft.ai.entity.AuditReport; import com.gxwebsoft.ai.enums.AuditReportEnum; import com.gxwebsoft.ai.service.AuditReportService; @@ -382,7 +385,6 @@ public class AuditReportController extends BaseController { sectionRun6.setFontSize(18); sectionRun6.setFontFamily("仿宋_GB2312"); sectionRun6.setColor("2c3e50"); - document.createParagraph(); // 添加固定内容(两段话) addOtherMattersSectionFixedContent(document, project); @@ -563,7 +565,6 @@ public class AuditReportController extends BaseController { sectionRun6.setFontSize(18); sectionRun6.setFontFamily("仿宋_GB2312"); sectionRun6.setColor("2c3e50"); - document.createParagraph(); // 添加固定内容(两段话) addOtherMattersSectionFixedContent(document, project); @@ -699,7 +700,7 @@ public class AuditReportController extends BaseController { XWPFParagraph sectionPara3 = document.createParagraph(); sectionPara3.setAlignment(ParagraphAlignment.LEFT); XWPFRun sectionRun3 = sectionPara3.createRun(); - sectionRun3.setText(String.format("三、%s同志主要业绩", project.getPersonName())); + sectionRun3.setText(String.format("三、%s主要业绩", project.getPersonName())); sectionRun3.setBold(true); sectionRun3.setFontSize(18); sectionRun3.setFontFamily("仿宋_GB2312"); @@ -891,40 +892,9 @@ public class AuditReportController extends BaseController { * 添加签名页(尾页)- 从 doc/尾页.docx 读取 */ private void addSignaturePage(XWPFDocument document) { - try (InputStream is = this.getClass().getResourceAsStream("/doc/尾页.docx")) { - if (is == null) { - log.warn("未找到尾页模板文件,使用默认表格方式创建"); - createSignaturePageWithTable(document); - return; - } - - XWPFDocument signatureDoc = new XWPFDocument(is); - - // 使用底层 XML 复制方式,确保样式 100% 保留 - // 1. 复制所有段落 - 直接追加到文档末尾 - for (XWPFParagraph srcPara : signatureDoc.getParagraphs()) { - document.createParagraph(); - int paraPos = document.getParagraphPos(document.getParagraphs().size() - 1); - org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP ctp = (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP)srcPara.getCTP().copy(); - document.getDocument().getBody().setPArray(paraPos, ctp); - } - - // 2. 复制所有表格 - for (XWPFTable srcTable : signatureDoc.getTables()) { - document.createTable(); - org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl cttbl = (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl)srcTable.getCTTbl().copy(); - // 获取文档中所有表格的 XML 数组并替换最后一个 - org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody body = document.getDocument().getBody(); - // 添加新的 tbl 元素 - body.addNewTbl(); - body.setTblArray(body.getTblArray().length - 1, cttbl); - } - - log.info("成功添加尾页(从 doc/尾页.docx 读取,使用底层 XML 复制)"); - } catch (IOException e) { - log.error("读取尾页文档失败", e); - createSignaturePageWithTable(document); - } + // 直接使用代码绘制签名页,避免复制模板带来的格式问题 + createSignaturePageWithTable(document); + log.info("成功添加尾页(使用代码直接绘制)"); } /** @@ -1051,10 +1021,12 @@ public class AuditReportController extends BaseController { // 创建一个 3 行 3 列的表格 XWPFTable table = document.createTable(3, 3); - // 设置表格宽度为 100%(自适应文档宽度) - // 表格宽度单位:二十分之一磅,A4 纸默认宽度约 9696(约 485 磅) + // 设置表格样式 - 添加边框 CTTbl ctTbl = table.getCTTbl(); CTTblPr tblPr = ctTbl.getTblPr(); + + // 设置表格宽度为 100%(自适应文档宽度) + // 表格宽度单位:二十分之一磅,A4 纸默认宽度约 9696(约 485 磅) CTTblWidth tblWidth = tblPr.getTblW(); if (tblWidth == null) { tblWidth = tblPr.addNewTblW(); @@ -1065,10 +1037,49 @@ public class AuditReportController extends BaseController { // 设置表格右对齐 tblPr.addNewJc().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STJc.RIGHT); + // 设置表格边框 - 保留所有边框,包括内部横线 + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders borders = tblPr.addNewTblBorders(); + // 上边框(实线) + borders.addNewTop().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder.SINGLE); + borders.getTop().setSz(java.math.BigInteger.valueOf(4)); + borders.getTop().setSpace(java.math.BigInteger.valueOf(0)); + // 下边框(实线) + borders.addNewBottom().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder.SINGLE); + borders.getBottom().setSz(java.math.BigInteger.valueOf(4)); + borders.getBottom().setSpace(java.math.BigInteger.valueOf(0)); + // 左边框(实线) + borders.addNewLeft().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder.SINGLE); + borders.getLeft().setSz(java.math.BigInteger.valueOf(4)); + borders.getLeft().setSpace(java.math.BigInteger.valueOf(0)); + // 右边框(实线) + borders.addNewRight().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder.SINGLE); + borders.getRight().setSz(java.math.BigInteger.valueOf(4)); + borders.getRight().setSpace(java.math.BigInteger.valueOf(0)); + // 内部横线(实线)- 第 2 行和第 3 行之间需要横线 + borders.addNewInsideH().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder.SINGLE); + borders.getInsideH().setSz(java.math.BigInteger.valueOf(4)); + borders.getInsideH().setSpace(java.math.BigInteger.valueOf(0)); + // 内部竖线(实线) + borders.addNewInsideV().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder.SINGLE); + borders.getInsideV().setSz(java.math.BigInteger.valueOf(4)); + borders.getInsideV().setSpace(java.math.BigInteger.valueOf(0)); + + // 设置列宽比例(按照模板文件设置) + // 第 1 列:XX 会计师事务所 - 约 15% 宽度 + // 第 2 列:中国注册会计师 - 约 25% 宽度 + // 第 3 列:签名区域 - 约 60% 宽度 + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid tblGrid = ctTbl.addNewTblGrid(); + // 第 1 列宽度(约 1500 twips = 75 磅) + tblGrid.addNewGridCol().setW(java.math.BigInteger.valueOf(1500)); + // 第 2 列宽度(约 2500 twips = 125 磅) + tblGrid.addNewGridCol().setW(java.math.BigInteger.valueOf(2500)); + // 第 3 列宽度(约 6000 twips = 300 磅) + tblGrid.addNewGridCol().setW(java.math.BigInteger.valueOf(6000)); + // 第一行 XWPFTableRow row1 = table.getRow(0); - // 设置第一行高度为 5 行字(12px * 5 = 60 磅,1 磅=1/72 英寸) - row1.setHeight(4320); // 单位是二十分之一磅,60 磅 * 72 = 4320 + // 设置第一行高度为 9 行字(12 磅 * 9 = 108 磅,1 磅=1/72 英寸) + row1.setHeight(7776); // 单位是二十分之一磅,108 磅 * 72 = 7776 // 第一行第一列:XX 会计师事务所(仿宋_GB2312,小四)- 跨两行合并 XWPFTableCell cell1_1 = row1.getCell(0); @@ -1087,27 +1098,97 @@ public class AuditReportController extends BaseController { run1_1.setFontFamily("仿宋_GB2312"); para1_1.setIndentationFirstLine(480); - // 第一行第二列:中国注册会计师(仿宋_GB2312,小四) + // 第一行第二列:中国注册会计师(仿宋_GB2312,小四)- 跨两行合并 XWPFTableCell cell1_2 = row1.getCell(1); - XWPFParagraph para1_2 = cell1_2.getParagraphs().get(0); - XWPFRun run1_2 = para1_2.createRun(); - run1_2.setText("中国注册会计师"); - run1_2.setFontSize(12); // 小四 - run1_2.setFontFamily("仿宋_GB2312"); + // 设置垂直合并(向下合并) + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr tcPr1_2 = cell1_2.getCTTc().getTcPr(); + if (tcPr1_2 == null) { + tcPr1_2 = cell1_2.getCTTc().addNewTcPr(); + } + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge vMerge1_2 = org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge.Factory.newInstance(); + vMerge1_2.setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.RESTART); + tcPr1_2.setVMerge(vMerge1_2); - // 第一行第三列:签名线(仿宋_GB2312,小四) + // 第 2 列内容:两个"中国注册会计师"之间间隔 7 个空行 + // 第一个"中国注册会计师" + XWPFParagraph para1_2_first = cell1_2.addParagraph(); + XWPFRun run1_2_first = para1_2_first.createRun(); + run1_2_first.setText("中国注册会计师"); + run1_2_first.setFontSize(12); // 小四 + run1_2_first.setFontFamily("仿宋_GB2312"); + + // 添加 7 个空行(原来是 6 个,现在增加 1 个) + for (int i = 0; i < 7; i++) { + XWPFParagraph emptyPara = cell1_2.addParagraph(); + emptyPara.createRun(); + } + + // 第二个"中国注册会计师" + XWPFParagraph para1_2_second = cell1_2.addParagraph(); + XWPFRun run1_2_second = para1_2_second.createRun(); + run1_2_second.setText("中国注册会计师"); + run1_2_second.setFontSize(12); // 小四 + run1_2_second.setFontFamily("仿宋_GB2312"); + + // 第一行第三列:签名线(仿宋_GB2312,小四)- 跨两行合并 - 先画下划线,再写 XXX XWPFTableCell cell1_3 = row1.getCell(2); - XWPFParagraph para1_3 = cell1_3.getParagraphs().get(0); - XWPFRun run1_3 = para1_3.createRun(); - run1_3.setText("XXX"); - run1_3.setFontSize(12); // 小四 - run1_3.setFontFamily("仿宋_GB2312"); - para1_3.setAlignment(ParagraphAlignment.CENTER); + // 设置垂直合并(向下合并) + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr tcPr1_3 = cell1_3.getCTTc().getTcPr(); + if (tcPr1_3 == null) { + tcPr1_3 = cell1_3.getCTTc().addNewTcPr(); + } + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge vMerge1_3 = org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge.Factory.newInstance(); + vMerge1_3.setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.RESTART); + tcPr1_3.setVMerge(vMerge1_3); + + // 第 3 列内容: + // 第一段:下划线(空行) + XWPFParagraph para1_3_line1 = cell1_3.addParagraph(); + XWPFRun run1_3_line1 = para1_3_line1.createRun(); + run1_3_line1.setText("____________________"); // 用下划线字符画线 + run1_3_line1.setFontSize(12); // 小四 + run1_3_line1.setFontFamily("仿宋_GB2312"); + para1_3_line1.setAlignment(ParagraphAlignment.CENTER); + + // 第二段:XXX + XWPFParagraph para1_3_name1 = cell1_3.addParagraph(); + XWPFRun run1_3_name1 = para1_3_name1.createRun(); + run1_3_name1.setText("XXX"); + run1_3_name1.setFontSize(12); // 小四 + run1_3_name1.setFontFamily("仿宋_GB2312"); + para1_3_name1.setAlignment(ParagraphAlignment.CENTER); + + // 添加 5 个空行(让第二条下划线比第 2 列的第二个“中国注册会计师”高一行) + for (int i = 0; i < 5; i++) { + XWPFParagraph emptyPara = cell1_3.addParagraph(); + emptyPara.createRun(); + } + + // 第三段:下划线(空行) + XWPFParagraph para1_3_line2 = cell1_3.addParagraph(); + XWPFRun run1_3_line2 = para1_3_line2.createRun(); + run1_3_line2.setText("____________________"); // 用下划线字符画线 + run1_3_line2.setFontSize(12); // 小四 + run1_3_line2.setFontFamily("仿宋_GB2312"); + para1_3_line2.setAlignment(ParagraphAlignment.CENTER); + + // 第四段:XXX + XWPFParagraph para1_3_name2 = cell1_3.addParagraph(); + XWPFRun run1_3_name2 = para1_3_name2.createRun(); + run1_3_name2.setText("XXX"); + run1_3_name2.setFontSize(12); // 小四 + run1_3_name2.setFontFamily("仿宋_GB2312"); + para1_3_name2.setAlignment(ParagraphAlignment.CENTER); + + // XXX 下方添加 6 个空行(距离底部边框) + for (int i = 0; i < 6; i++) { + XWPFParagraph emptyPara = cell1_3.addParagraph(); + emptyPara.createRun(); + } // 第二行 XWPFTableRow row2 = table.getRow(1); - // 设置第二行高度为 5 行字(12px * 5 = 60 磅) - row2.setHeight(4320); // 单位是二十分之一磅,60 磅 * 72 = 4320 + // 不设置固定高度,让行高自适应内容 // 第二行第一列:被合并(不显示内容) XWPFTableCell cell2_1 = row2.getCell(0); @@ -1120,22 +1201,27 @@ public class AuditReportController extends BaseController { vMerge2.setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.CONTINUE); tcPr2_1.setVMerge(vMerge2); - // 第二行第二列:中国注册会计师(仿宋_GB2312,小四) + // 第二行第二列:被合并(不显示内容) XWPFTableCell cell2_2 = row2.getCell(1); - XWPFParagraph para2_2 = cell2_2.getParagraphs().get(0); - XWPFRun run2_2 = para2_2.createRun(); - run2_2.setText("中国注册会计师"); - run2_2.setFontSize(12); // 小四 - run2_2.setFontFamily("仿宋_GB2312"); + // 设置垂直合并(继续上一行的合并) + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr tcPr2_2 = cell2_2.getCTTc().getTcPr(); + if (tcPr2_2 == null) { + tcPr2_2 = cell2_2.getCTTc().addNewTcPr(); + } + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge vMerge2_2 = org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge.Factory.newInstance(); + vMerge2_2.setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.CONTINUE); + tcPr2_2.setVMerge(vMerge2_2); - // 第二行第三列:签名(仿宋_GB2312,小四) + // 第二行第三列:被合并(不显示内容) XWPFTableCell cell2_3 = row2.getCell(2); - XWPFParagraph para2_3 = cell2_3.getParagraphs().get(0); - XWPFRun run2_3 = para2_3.createRun(); - run2_3.setText("XXX"); - run2_3.setFontSize(12); // 小四 - run2_3.setFontFamily("仿宋_GB2312"); - para2_3.setAlignment(ParagraphAlignment.CENTER); + // 设置垂直合并(继续上一行的合并) + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr tcPr2_3 = cell2_3.getCTTc().getTcPr(); + if (tcPr2_3 == null) { + tcPr2_3 = cell2_3.getCTTc().addNewTcPr(); + } + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge vMerge2_3 = org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge.Factory.newInstance(); + vMerge2_3.setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.CONTINUE); + tcPr2_3.setVMerge(vMerge2_3); // 第三行 - 使用 gridAfter 来正确合并单元格 XWPFTableRow row3 = table.getRow(2); @@ -1184,10 +1270,16 @@ public class AuditReportController extends BaseController { private void addBodyTitle(XWPFDocument document, String title) throws IOException { XWPFParagraph titlePara = document.createParagraph(); titlePara.setAlignment(ParagraphAlignment.CENTER); + + // 设置行距为 1.5 倍 + CTSpacing spacing = titlePara.getCTP().addNewPPr().addNewSpacing(); + spacing.setLineRule(org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule.AUTO); + spacing.setLine(java.math.BigInteger.valueOf(360)); // 1.5 倍行距 + XWPFRun titleRun = titlePara.createRun(); titleRun.setText(title); titleRun.setBold(true); - titleRun.setFontSize(16); // 三号 + titleRun.setFontSize(16); // 三号 (16 磅) titleRun.setFontFamily("仿宋_GB2312"); // 添加 2 个空行 @@ -1218,6 +1310,12 @@ public class AuditReportController extends BaseController { private void addDocumentNumber(XWPFDocument document, String docNumber) throws IOException { XWPFParagraph para = document.createParagraph(); para.setAlignment(ParagraphAlignment.RIGHT); + + // 设置行距为 1.5 倍 + CTSpacing spacing = para.getCTP().addNewPPr().addNewSpacing(); + spacing.setLineRule(org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule.AUTO); + spacing.setLine(java.math.BigInteger.valueOf(360)); // 1.5 倍行距 + XWPFRun run = para.createRun(); run.setText(docNumber); run.setFontSize(12); // 小四 @@ -1235,6 +1333,12 @@ public class AuditReportController extends BaseController { private void addClientUnit(XWPFDocument document, PwlProject project) throws IOException { XWPFParagraph para = document.createParagraph(); para.setAlignment(ParagraphAlignment.LEFT); + + // 设置行距为 1.5 倍 + CTSpacing spacing = para.getCTP().addNewPPr().addNewSpacing(); + spacing.setLineRule(org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule.AUTO); + spacing.setLine(java.math.BigInteger.valueOf(360)); // 1.5 倍行距 + XWPFRun run = para.createRun(); run.setText(project.getName()+":"); run.setFontSize(12); // 小四 @@ -1262,8 +1366,14 @@ public class AuditReportController extends BaseController { // 整段话作为一个段落(仿宋_GB2312,小四) XWPFParagraph para = document.createParagraph(); para.setAlignment(ParagraphAlignment.LEFT); + + // 设置行距为 1.5 倍 + CTSpacing spacing = para.getCTP().addNewPPr().addNewSpacing(); + spacing.setLineRule(org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule.AUTO); + spacing.setLine(java.math.BigInteger.valueOf(360)); // 1.5 倍行距 + XWPFRun run = para.createRun(); - run.setText(String.format(intro, project.getName(), project.getPersonName(), project.getPosition(), project.getPersonName())); + run.setText(String.format(intro, project.getName(), project.getPersonName(), project.getPosition(), project.getPersonName()));; run.setFontSize(12); // 小四 run.setFontFamily("仿宋_GB2312"); @@ -1304,9 +1414,6 @@ public class AuditReportController extends BaseController { sectionRun.setFontSize(16); // 三号 sectionRun.setFontFamily("仿宋_GB2312"); - // 添加空行 - document.createParagraph(); - // 添加子章节 if (subSections != null && !subSections.isEmpty()) { for (String subSection : subSections) { @@ -1378,9 +1485,6 @@ public class AuditReportController extends BaseController { sectionRun.setFontSize(16); // 三号 sectionRun.setFontFamily("仿宋_GB2312"); - // 添加空行 - document.createParagraph(); - // 添加子章节内容(根据固定的子章节结构) if (formCommits != null && !formCommits.isEmpty()) { for (Integer formCommit : formCommits) { @@ -1396,9 +1500,6 @@ public class AuditReportController extends BaseController { subsectionRun.setFontFamily("仿宋_GB2312"); subsectionPara.setIndentationFirstLine(480); - // 添加空行 - document.createParagraph(); - // 添加子章节内容(如果有) String content = contentMap.get(formCommit); if (content != null && !content.trim().isEmpty()) { @@ -1461,9 +1562,6 @@ public class AuditReportController extends BaseController { run1.setFontFamily("仿宋_GB2312"); para1.setIndentationFirstLine(480); - // 添加空行 - document.createParagraph(); - // (二)本报告仅供委托方 XX 单位了解 XX 同志经济责任履行情况之用,未经本所书面同意不得用作其他用途,因使用不当造成的后果,与执行本次专项审计业务的注册会计师及会计师事务所无关。(仿宋_GB2312,小四) XWPFParagraph para2 = document.createParagraph(); para2.setAlignment(ParagraphAlignment.LEFT); @@ -1472,9 +1570,6 @@ public class AuditReportController extends BaseController { run2.setFontSize(12); // 小四 run2.setFontFamily("仿宋_GB2312"); para2.setIndentationFirstLine(480); - - // 添加空行 - document.createParagraph(); } /** @@ -1592,32 +1687,40 @@ public class AuditReportController extends BaseController { */ @Operation(summary = "AI 生成默认话术") @PostMapping("/generateDefaultText") - public ApiResult generateDefaultText( - @RequestParam Integer projectId, - @RequestParam Integer formCommit, - @RequestParam(required = false) String chapterTitle) { + public ApiResult generateDefaultText(@RequestBody AuditReportDefaultTextRequest request) { try { final User loginUser = getLoginUser(); - log.info("AI 生成默认话术 - formCommit: {}, chapterTitle: {}", formCommit, chapterTitle); + log.info("AI 生成默认话术 - formCommit: {}, chapterTitle: {}", request.getFormCommit(), request.getChapterTitle()); // 获取章节标题 - String title = chapterTitle; + String title = request.getChapterTitle(); if (title == null || title.trim().isEmpty()) { - title = AuditReportEnum.getByCode(formCommit) != null - ? AuditReportEnum.getByCode(formCommit).getDesc() + title = AuditReportEnum.getByCode(request.getFormCommit()) != null + ? AuditReportEnum.getByCode(request.getFormCommit()).getDesc() : "审计内容"; } + + // 从请求体中获取选中的取证单 ID 列表 + List evidenceIds = new ArrayList<>(); + if (request.getEvidenceIds() != null && !request.getEvidenceIds().isEmpty()) { + evidenceIds = request.getEvidenceIds().stream() + .map(Long::valueOf) + .collect(Collectors.toList()); + } // 构建提示词,根据 formCommit 使用不同的默认 prompt - PwlProject project = pwlProjectService.getById(projectId); + PwlProject project = pwlProjectService.getById(request.getProjectId()); + if (project == null) { + return fail("项目不存在,projectId: " + request.getProjectId(), null); + } String companyName = project.getName(); String personName = project.getPersonName(); String prompt; - if (formCommit == 20) { + if (request.getFormCommit() == 20) { // 二、总体评价的特殊提示词模板 // 查询该项目的所有取证单,判断是否有发现问题 LambdaQueryWrapper evidenceWrapper = new LambdaQueryWrapper<>(); - evidenceWrapper.eq(AuditEvidence::getProjectId, projectId) + evidenceWrapper.eq(AuditEvidence::getProjectId, request.getProjectId()) .eq(AuditEvidence::getDeleted, 0); List allEvidences = auditEvidenceMapper.selectList(evidenceWrapper); @@ -1634,61 +1737,78 @@ public class AuditReportController extends BaseController { } prompt = promptBuilder.toString(); - } else if (formCommit == 30) { + } else if (request.getFormCommit() == 30) { // 三、XX 同志主要业绩的特殊提示词模板 prompt = String.format("请根据传入的取证单及相关资料库的数据,总结%s 任职期间的主要业绩。要求:1.基于审计事实和数据;2.突出重要贡献和成绩;3.内容客观真实;4.条理清晰。", personName); - } else if (formCommit == 41) { + } else if (request.getFormCommit() == 41) { // (二)XX 公司概况的特殊提示词模板 prompt = String.format("请生成关于'%s'的详细说明。要求按以下格式返回:\n" + "%s 为(说明企业性质,如:国有独资/控股公司),成立于 XXXX 年 X 月 X 日,注册资本**元,法定代表人 XX,统一社会信用代码:XXX。\n" + "公司主营业务为……(简述主营业务)。\n" + "%s 下设……(组织架构)。", title, companyName, companyName); - } else if (formCommit == 42) { + } else if (request.getFormCommit() == 42) { // (三)XX 同志任职及分工情况的特殊提示词模板 prompt = String.format("请生成关于《%s任职及分工情况》的详细说明。按以下格式返回:\n" + "%s(同本章节标题人名) 自 XXXX 年 X 月至 XXXX 年 X 月任 XXXX(单位名称)XXX(职务名称),其主要职责是:...(描述主要工作职责)。", personName, personName); - } else if (formCommit == 43) { + } else if (request.getFormCommit() == 43) { // (四)实施审计的基本情况,要求返回两段内容 prompt = String.format("请生成关于'%s'的详细说明。要求返回两段内容:\n" + "第一段格式:本次审计的时间范围是 20XX 年 XX 月 XX 日至 20XX 年 XX 月 XX 日。本次审计以 %s 会计报表、账簿、凭证及相关经济活动资料为基础,对 %s 任职期间履行经济责任的情况进行审查,主要包括:...(具体内容)。\n" + "第二段格式:%s 及 %s 对所提供的与审计相关的会计资料以及其他证明材料作出了书面承诺,对其真实性和完整性负责。我们按照审计实施方案确定的范围和内容,实施了在当时情况下认为有必要采取的审计程序和方法,包括(具体的程序和方案)等,并对重要事项进行了必要的延伸和追溯。", title, companyName, personName, companyName, personName); - } else if (formCommit >= 51 && formCommit <= 57) { + } else if (request.getFormCommit() >= 51 && request.getFormCommit() <= 57) { // 四、履行经济责任的主要情况及审计发现的问题与责任认定的所有子章节 // 要求返回三段:第一段概述和问题列示,第二段原因分析,第三段责任界定 - prompt = String.format("请生成关于'%s'的详细说明。要求返回三段内容:\n" + - "第一段(概述和问题列示):概述%s 任职期间履行经济责任所涉及的主要工作,重点说明其在职责范围内开展的经济活动和管理行为的核心内容;列示审计中发现的问题,例如:未严格落实国家相关政策要求、在某类业务操作中违反国有资产监管程序等,需列明具体事实、涉及金额以及所违反的具体规定条文;并对问题进行责任认定。\n" + - "第二段标题'原因分析:',针对上述问题,深入分析问题产生的原因,包括主观原因和客观原因,从制度机制、内部管理、人员素质等多个角度进行剖析。\n" + - "第三段标题'责任界定:',根据问题性质和情节轻重,结合%s 的职责分工,对其应承担的责任进行界定,明确是直接责任、主管责任还是领导责任,并说明认定依据。", - title, personName, personName); + StringBuilder promptBuilder = new StringBuilder(); + + // 只传递取证单 ID 列表,让 AI 自行从数据库读取 + if (evidenceIds != null && !evidenceIds.isEmpty()) { + // 将 ID 列表转换为逗号分隔的字符串 + String evidenceIdsStr = evidenceIds.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + + promptBuilder.append(String.format("取证单编号(仅用于查询数据,不要在输出中显示):%s\n", evidenceIdsStr)); + } + + // 生成指令 + promptBuilder.append(String.format("请根据上述取证单数据,为%s生成审计报告该章节内容。\n", personName)); + promptBuilder.append("要求生成三段:\n"); + promptBuilder.append("1. 履职情况 + 发现问题及责任认定\n"); + promptBuilder.append("2. 原因分析\n"); + promptBuilder.append("3. 责任界定\n"); + promptBuilder.append("\n注意:输出内容中不要显示取证单编号,直接生成正式的审计报告内容。\n"); + + prompt = promptBuilder.toString(); } else { // 其他章节使用通用 prompt - 查询审计相关法规 prompt = String.format("请查询与'%s'相关的所有审计法规、制度和政策文件。要求:1.列出完整的法规名称;2.注明颁布单位;3.注明发文字号(如有);4.列出相关条款;5.按重要性排序;6.使用规范的格式。", title); } - prompt += "(请注意:数据源都必须来自资料库或者数据库相关数据,不能从其他地方获取数据。)"; - log.info("生成审计报告标题:{},AI提示词: {}", chapterTitle, prompt); - // 调用 AI 接口 - String result = invokeDefaultTextGeneration(prompt, loginUser.getUsername()); + prompt += "\n(重要提示:数据源必须严格来自页面上选择的取证单数据,不能从其他地方获取数据。)"; + log.info("生成审计报告标题:{},AI 提示词:{}", request.getChapterTitle(), prompt); + + // 调用 AI 接口,同时传递取证单 ID 列表 + String result = invokeDefaultTextGeneration(prompt, loginUser.getUsername(), evidenceIds); // 保存历史记录到数据库 try { // 构建请求数据 JSONObject requestData = new JSONObject(); - requestData.put("projectId", projectId); - requestData.put("formCommit", formCommit); - requestData.put("chapterTitle", chapterTitle); - + requestData.put("projectId", request.getProjectId()); + requestData.put("formCommit", request.getFormCommit()); + requestData.put("chapterTitle", request.getChapterTitle()); + // 根据 formCommit 生成不同的接口名称 - String interfaceName = "/api/ai/auditReport/generateDefaultText_" + formCommit; - + String interfaceName = "/api/ai/auditReport/generateDefaultText_" + request.getFormCommit(); + // 生成请求哈希 String requestHash = DigestUtil.md5Hex(interfaceName + ":" + requestData.toJSONString()); - + // 保存历史记录 aiHistoryService.saveHistory( - Long.valueOf(projectId), + Long.valueOf(request.getProjectId()), requestHash, interfaceName, requestData.toJSONString(), @@ -1697,7 +1817,7 @@ public class AuditReportController extends BaseController { loginUser.getUsername(), loginUser.getTenantId() ); - log.info("保存 AI 生成历史记录成功 - projectId: {}, interfaceName: {}", projectId, interfaceName); + log.info("保存 AI 生成历史记录成功 - projectId: {}, interfaceName: {}", request.getProjectId(), interfaceName); } catch (Exception e) { log.warn("保存 AI 生成历史记录失败", e); } @@ -1749,7 +1869,7 @@ public class AuditReportController extends BaseController { /** * 调用 AI 生成默认话术 */ - private String invokeDefaultTextGeneration(String prompt, String userName) { + private String invokeDefaultTextGeneration(String prompt, String userName, List evidenceIds) { // 构建请求体 JSONObject requestBody = new JSONObject(); JSONObject inputs = new JSONObject(); @@ -1759,26 +1879,61 @@ public class AuditReportController extends BaseController { inputs.put("suggestion", ""); inputs.put("title", "审计报告默认话术生成"); + // 如果有取证单 ID,加入到请求中 + if (evidenceIds != null && !evidenceIds.isEmpty()) { + inputs.put("evidenceIds", evidenceIds); + } + requestBody.put("inputs", inputs); requestBody.put("response_mode", "blocking"); requestBody.put("user", userName); - // 发送 POST 请求 - String result = HttpUtil.createPost("http://1.14.159.185:8180/v1/workflows/run") - .header("Authorization", "Bearer app-d7Ok9FECVZG2Ybw9wpg7tGu9") - .header("Content-Type", "application/json") - .body(requestBody.toString()) - .timeout(600000) - .execute() - .body(); - - // 解析返回的 JSON 字符串 - JSONObject jsonResponse = JSONObject.parseObject(result); - JSONObject data = jsonResponse.getJSONObject("data"); - JSONObject outputs = data.getJSONObject("outputs"); - String resultStr = outputs.getString("result"); - - return resultStr != null ? resultStr : ""; + try { + // 发送 POST 请求 + String result = HttpUtil.createPost("http://1.14.159.185:8180/v1/workflows/run") + .header("Authorization", "Bearer app-d7Ok9FECVZG2Ybw9wpg7tGu9") + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .timeout(600000) + .execute() + .body(); + + log.info("AI 工作流返回结果:{}", result); + + // 解析返回的 JSON 字符串 + JSONObject jsonResponse = JSONObject.parseObject(result); + + // 检查是否有错误 + if (jsonResponse.containsKey("error")) { + JSONObject error = jsonResponse.getJSONObject("error"); + String errorMsg = error != null ? error.getString("message") : "未知错误"; + throw new RuntimeException("AI 服务返回错误:" + errorMsg); + } + + JSONObject data = jsonResponse.getJSONObject("data"); + if (data == null) { + log.error("AI 响应中缺少 data 字段,完整响应:{}", result); + throw new RuntimeException("AI 服务响应格式异常,缺少 data 字段"); + } + + JSONObject outputs = data.getJSONObject("outputs"); + if (outputs == null) { + log.error("AI 响应中缺少 outputs 字段,完整响应:{}", result); + throw new RuntimeException("AI 服务响应格式异常,缺少 outputs 字段"); + } + + String resultStr = outputs.getString("result"); + + if (resultStr == null || resultStr.trim().isEmpty()) { + log.warn("AI 返回的结果为空,完整响应:{}", result); + return ""; + } + + return resultStr; + } catch (Exception e) { + log.error("调用 AI 工作流失败", e); + throw new RuntimeException("调用 AI 工作流失败:" + e.getMessage(), e); + } } /** @@ -1798,22 +1953,52 @@ public class AuditReportController extends BaseController { requestBody.put("response_mode", "blocking"); requestBody.put("user", userName); - // 发送 POST 请求 - String result = HttpUtil.createPost("http://1.14.159.185:8180/v1/workflows/run") - .header("Authorization", "Bearer app-d7Ok9FECVZG2Ybw9wpg7tGu9") - .header("Content-Type", "application/json") - .body(requestBody.toString()) - .timeout(600000) - .execute() - .body(); - - // 解析返回的 JSON 字符串 - JSONObject jsonResponse = JSONObject.parseObject(result); - JSONObject data = jsonResponse.getJSONObject("data"); - JSONObject outputs = data.getJSONObject("outputs"); - String resultStr = outputs.getString("result"); - - return resultStr != null ? resultStr : ""; + try { + // 发送 POST 请求 + String result = HttpUtil.createPost("http://1.14.159.185:8180/v1/workflows/run") + .header("Authorization", "Bearer app-d7Ok9FECVZG2Ybw9wpg7tGu9") + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .timeout(600000) + .execute() + .body(); + + log.info("AI 工作流返回结果:{}", result); + + // 解析返回的 JSON 字符串 + JSONObject jsonResponse = JSONObject.parseObject(result); + + // 检查是否有错误 + if (jsonResponse.containsKey("error")) { + JSONObject error = jsonResponse.getJSONObject("error"); + String errorMsg = error != null ? error.getString("message") : "未知错误"; + throw new RuntimeException("AI 服务返回错误:" + errorMsg); + } + + JSONObject data = jsonResponse.getJSONObject("data"); + if (data == null) { + log.error("AI 响应中缺少 data 字段,完整响应:{}", result); + throw new RuntimeException("AI 服务响应格式异常,缺少 data 字段"); + } + + JSONObject outputs = data.getJSONObject("outputs"); + if (outputs == null) { + log.error("AI 响应中缺少 outputs 字段,完整响应:{}", result); + throw new RuntimeException("AI 服务响应格式异常,缺少 outputs 字段"); + } + + String resultStr = outputs.getString("result"); + + if (resultStr == null || resultStr.trim().isEmpty()) { + log.warn("AI 返回的结果为空,完整响应:{}", result); + return ""; + } + + return resultStr; + } catch (Exception e) { + log.error("调用 AI 工作流失败", e); + throw new RuntimeException("调用 AI 工作流失败:" + e.getMessage(), e); + } } /** @@ -1877,8 +2062,8 @@ public class AuditReportController extends BaseController { suggestionsBuilder.toString() ); - // 调用 AI 接口 - String result = invokeDefaultTextGeneration(prompt, loginUser.getUsername()); + // 调用 AI 接口,传递取证单 ID + String result = invokeDefaultTextGeneration(prompt, loginUser.getUsername(), evidenceIds.stream().map(Long::valueOf).collect(Collectors.toList())); return success(result); } catch (Exception e) { diff --git a/src/main/java/com/gxwebsoft/ai/dto/AuditReportDefaultTextRequest.java b/src/main/java/com/gxwebsoft/ai/dto/AuditReportDefaultTextRequest.java new file mode 100644 index 0000000..bb9ebea --- /dev/null +++ b/src/main/java/com/gxwebsoft/ai/dto/AuditReportDefaultTextRequest.java @@ -0,0 +1,26 @@ +package com.gxwebsoft.ai.dto; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AI 生成默认话术请求 DTO + */ +@Schema(description = "AI 生成默认话术请求") +@Data +public class AuditReportDefaultTextRequest { + + @Schema(description = "项目 ID", required = true) + private Integer projectId; + + @Schema(description = "章节类型编码(1-11)", required = true) + private Integer formCommit; + + @Schema(description = "章节标题") + private String chapterTitle; + + @Schema(description = "选中的取证单 ID 列表") + private List evidenceIds; +} diff --git a/src/main/java/com/gxwebsoft/ai/service/impl/AuditReportServiceImpl.java b/src/main/java/com/gxwebsoft/ai/service/impl/AuditReportServiceImpl.java index cb4843d..f97fed8 100644 --- a/src/main/java/com/gxwebsoft/ai/service/impl/AuditReportServiceImpl.java +++ b/src/main/java/com/gxwebsoft/ai/service/impl/AuditReportServiceImpl.java @@ -64,11 +64,11 @@ public class AuditReportServiceImpl extends ServiceImpl> sectionOrder = new ArrayList<>(); sectionOrder.add(createSectionInfo(1, "基本情况", Arrays.asList(41, 42, 43))); sectionOrder.add(createSectionInfo(2, "总体评价", null)); - sectionOrder.add(createSectionInfo(3, String.format("%s同志主要业绩", project.getPersonName()), null)); + sectionOrder.add(createSectionInfo(3, String.format("%s主要业绩", project.getPersonName()), null)); sectionOrder.add(createSectionInfo(4, "履行经济责任的主要情况及审计发现的问题与责任认定", Arrays.asList(51, 52, 53, 54, 55, 56, 57))); sectionOrder.add(createSectionInfo(5, "审计建议", null)); sectionOrder.add(createSectionInfo(6, "其他事项说明", null)); diff --git a/src/main/java/com/gxwebsoft/common/core/config/TomcatConfig.java b/src/main/java/com/gxwebsoft/common/core/config/TomcatConfig.java new file mode 100644 index 0000000..0861a74 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/core/config/TomcatConfig.java @@ -0,0 +1,25 @@ +package com.gxwebsoft.common.core.config; + +import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Tomcat 配置类,用于解决 URL 中包含特殊字符的问题 + */ +@Configuration +public class TomcatConfig { + + /** + * 允许 URL 中包含特殊字符(如中文、括号等) + * 解决错误:Invalid character found in the request target + */ + @Bean + public TomcatConnectorCustomizer tomcatConnectorCustomizer() { + return connector -> { + // 允许 [] 等特殊字符 + connector.setProperty("relaxedPathChars", "[]|{}^<>\""); + connector.setProperty("relaxedQueryChars", "[]|{}^<>\"\\`"); + }; + } +}