Compare commits

..

2 Commits

Author SHA1 Message Date
0a941e1a8a 更新 2026-07-06 10:40:09 +08:00
98250fca47 优化 2026-06-18 09:20:32 +08:00
9 changed files with 180 additions and 47 deletions

View File

@@ -21,7 +21,8 @@ public enum EsignConfigConstants {
ESIGN_APP_SECRETMAP("325c8cc6c2a303d6cf1fb5657a16e591","e签宝appIDSecret映射"), ESIGN_APP_SECRETMAP("325c8cc6c2a303d6cf1fb5657a16e591","e签宝appIDSecret映射"),
ENABLEANTIFAKESIGN("","是否加盖防伪章"), ENABLEANTIFAKESIGN("","是否加盖防伪章"),
FIXAPPUNIT("true","是否固定要用的E签宝应用主体单位"), FIXAPPUNIT("true","是否固定要用的E签宝应用主体单位"),
FIXAPPUNITNAME("","固定要用的E签宝应用主体单位名称") FIXAPPUNITNAME("","固定要用的E签宝应用主体单位名称"),
SIGNPARAM_LOG_URL("","E签宝参数记录路径"),
; ;
private String defaultValue; private String defaultValue;

View File

@@ -10,11 +10,11 @@ import java.util.Map;
public class EsignParamRecordUtils { public class EsignParamRecordUtils {
public static void saveEsignParamRecord(Map<String,Object> params) { public static void saveEsignParamRecord(String logFileUrl,Map<String,Object> params) {
try { try {
String str = "/home/seeyon/Seeyon/A8/ApacheJetspeed/logs_sy"; String str = logFileUrl;
String date = DateUtil.format(new Date(), "yyyyMMddHHmmss"); String date = DateUtil.format(new Date(), "yyyyMMddHHmmss");
FileUtils.writeStringToFile(new File(str+"/esignParams/" + date + ".json"), JsonUtils.toJSONString(params), "UTF-8"); FileUtils.writeStringToFile(new File(str+ File.separator + "esignParams" + File.separator + date + ".json"), JsonUtils.toJSONString(params), "UTF-8");
}catch (Exception e){ }catch (Exception e){
} }

View File

@@ -13,6 +13,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.util.List; import java.util.List;
@@ -45,13 +46,28 @@ public class A4CheckNode extends ACommonSuperNode {
List<String> paths = fileUtil.fieldFileDownload(Long.parseLong(attachmentId), tempDir + File.separator + "oafile" + File.separator); List<String> paths = fileUtil.fieldFileDownload(Long.parseLong(attachmentId), tempDir + File.separator + "oafile" + File.separator);
for (String path : paths) { for (String path : paths) {
file = new File(path); file = new File(path);
boolean firstPageA4 = DocumentA4Checker.isFirstPageA4(file); try {
if (!firstPageA4) { String fileName = file.getName().toLowerCase();
String errMsg = "上传的附件中" + file.getName() +"非标准A4格式的请修改后再上传(标准A4为21:29)"; // 检测 docx 批注/痕迹会导致转PDF后页面撑大不再是A4
formDataVo.getNewFieldDataMap().put("合同审批附件格式校验结果", errMsg); if (fileName.endsWith(".docx") || fileName.endsWith(".doc")) {
return context.back(errMsg); try (FileInputStream fis = new FileInputStream(file)) {
if (DocumentA4Checker.hasCommentsOrTrackChanges(fis)) {
String errMsg = "上传的附件" + file.getName() + "包含批注或修订痕迹,请清除后再上传";
formDataVo.getNewFieldDataMap().put("合同审批附件格式校验结果", errMsg);
return context.back(errMsg);
}
}
}
boolean firstPageA4 = DocumentA4Checker.isFirstPageA4(file);
if (!firstPageA4) {
String errMsg = "上传的附件中" + file.getName() + "非标准A4格式的请修改后再上传(标准A4为210:297)";
formDataVo.getNewFieldDataMap().put("合同审批附件格式校验结果", errMsg);
return context.back(errMsg);
}
formDataVo.getNewFieldDataMap().put("合同审批附件格式校验结果", "");
} finally {
file.delete();
} }
file.delete();
} }
return context.success("文件A4格式校验通过"); return context.success("文件A4格式校验通过");

View File

@@ -121,7 +121,7 @@ public class EsignMultipleSignerNode extends ACommonSuperNode {
aParty.setPartyId("甲方"); aParty.setPartyId("甲方");
aParty.setSignOrder(2); aParty.setSignOrder(2);
aParty.setSignerType(SignerType.ORG); aParty.setSignerType(SignerType.ORG);
aParty.setSealId(getStringField(formDataVo,"选择甲方印章")); aParty.setSealId(getStringField(formDataVo,"甲方印章ID"));
aParty.setAutoSign(doAutoSign); aParty.setAutoSign(doAutoSign);
OrgInfo orgInfo = new OrgInfo(); OrgInfo orgInfo = new OrgInfo();
aParty.setOrgInfo(orgInfo); aParty.setOrgInfo(orgInfo);

View File

@@ -129,7 +129,7 @@ public class EsignOneSignerNode extends ACommonSuperNode {
aParty.setSignOrder(2); aParty.setSignOrder(2);
aParty.setSignerType(SignerType.ORG); aParty.setSignerType(SignerType.ORG);
aParty.setAutoSign(doAutoSign); aParty.setAutoSign(doAutoSign);
aParty.setSealId(getStringField(formDataVo,"选择甲方印章")); aParty.setSealId(getStringField(formDataVo,"甲方印章ID"));
OrgInfo orgInfo = new OrgInfo(); OrgInfo orgInfo = new OrgInfo();
orgInfo.setOrgName(orgName); orgInfo.setOrgName(orgName);
aParty.setOrgInfo(orgInfo); aParty.setOrgInfo(orgInfo);
@@ -208,7 +208,7 @@ public class EsignOneSignerNode extends ACommonSuperNode {
param.setPageSize(1); param.setPageSize(1);
param.setOrderField("UPDATE_DATE"); param.setOrderField("UPDATE_DATE");
param.setSortType("DESC"); param.setSortType("DESC");
Map<String, Object> sqlMap = FormTableExecutor.generateSql(param); Map<String, Object> sqlMap = SqlGenerator.generateSql(param);
JDBCAgent agent = new JDBCAgent(); JDBCAgent agent = new JDBCAgent();
try { try {
agent.execute((String)sqlMap.get("sql"), (List)sqlMap.get("params")); agent.execute((String)sqlMap.get("sql"), (List)sqlMap.get("params"));

View File

@@ -321,7 +321,7 @@ public class DefaultSignerBuilder implements SignerBuilder {
} }
/** /**
* 查询文件总页数 * 查询文件总页数查询失败或页数为0时返回默认值10
*/ */
private Integer getFileTotalPage(String fileId, String appId) { private Integer getFileTotalPage(String fileId, String appId) {
try { try {
@@ -336,14 +336,18 @@ public class DefaultSignerBuilder implements SignerBuilder {
EsignBaseResp resp = JsonUtils.parseObject(respStr, EsignBaseResp.class); EsignBaseResp resp = JsonUtils.parseObject(respStr, EsignBaseResp.class);
if (!"0".equals(String.valueOf(resp.getCode()))) { if (!"0".equals(String.valueOf(resp.getCode()))) {
throw new RuntimeException("查询文件上传状态失败:" + resp.getMessage()); return 10;
} }
Map<String, Object> data = (Map<String, Object>) resp.getData(); Map<String, Object> data = (Map<String, Object>) resp.getData();
Object pageCount = data.get("fileTotalPageCount"); Object pageCount = data.get("fileTotalPageCount");
return pageCount == null ? 10 : Integer.parseInt(String.valueOf(pageCount)); if (pageCount == null) {
return 10;
}
int pages = Integer.parseInt(String.valueOf(pageCount));
return pages > 0 ? pages : 10;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("获取文件页数异常", e); return 10;
} }
} }

View File

@@ -1,6 +1,5 @@
package com.seeyon.apps.esign.po.signer; package com.seeyon.apps.esign.po.signer;
import com.seeyon.apps.esign.config.EsignConfigProvider;
import com.seeyon.apps.esign.po.signfield.NormalSignFieldConfig; import com.seeyon.apps.esign.po.signfield.NormalSignFieldConfig;
import com.seeyon.apps.esign.po.signfield.SignField; import com.seeyon.apps.esign.po.signfield.SignField;
import com.seeyon.apps.esign.po.signfield.SignFieldPosition; import com.seeyon.apps.esign.po.signfield.SignFieldPosition;
@@ -13,8 +12,6 @@ import java.util.Map;
public class JsonSignerBuilder implements SignerBuilder{ public class JsonSignerBuilder implements SignerBuilder{
private EsignConfigProvider esignConfigProvider = EsignConfigProvider.getInstance();
@Override @Override
public Signer build(List<String> fileIds, List<Object> keywordPos, SignParty party,String appId) { public Signer build(List<String> fileIds, List<Object> keywordPos, SignParty party,String appId) {
if (party.getSignerType() == SignerType.PERSON) { if (party.getSignerType() == SignerType.PERSON) {

View File

@@ -4,6 +4,7 @@ import com.seeyon.aicloud.common.JsonUtils;
import com.seeyon.apps.esign.config.EsignConfigProvider; import com.seeyon.apps.esign.config.EsignConfigProvider;
import com.seeyon.apps.esign.constants.EsignApiUrl; import com.seeyon.apps.esign.constants.EsignApiUrl;
import com.seeyon.apps.esign.constants.EsignConfigConstants; import com.seeyon.apps.esign.constants.EsignConfigConstants;
import com.seeyon.apps.esign.logfile.EsignParamRecordUtils;
import com.seeyon.apps.esign.po.esignapi.EsignApiHeader; import com.seeyon.apps.esign.po.esignapi.EsignApiHeader;
import com.seeyon.apps.esign.po.esignapi.EsignBaseResp; import com.seeyon.apps.esign.po.esignapi.EsignBaseResp;
import com.seeyon.apps.esign.utils.HttpClient; import com.seeyon.apps.esign.utils.HttpClient;
@@ -42,6 +43,7 @@ public class EsignByTemplateService {
esignApiHeader.token(tokenCacheManager.getToken(appId)).appId(appId); esignApiHeader.token(tokenCacheManager.getToken(appId)).appId(appId);
String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.SIGN_BY_FILE_URL; String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.SIGN_BY_FILE_URL;
log.info("合同发起入参: " + jsonBody); log.info("合同发起入参: " + jsonBody);
EsignParamRecordUtils.saveEsignParamRecord(configProvider.getBizConfigByKey(EsignConfigConstants.SIGNPARAM_LOG_URL),params);
String respStr = HttpClient.httpPostRaw(url, jsonBody, esignApiHeader.convert2Headers(), "UTF-8"); String respStr = HttpClient.httpPostRaw(url, jsonBody, esignApiHeader.convert2Headers(), "UTF-8");
EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class); EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class);
Map<String,Object> dataMap = (Map<String, Object>) esignBaseResp.getData(); Map<String,Object> dataMap = (Map<String, Object>) esignBaseResp.getData();

View File

@@ -4,13 +4,16 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.usermodel.Section; import org.apache.poi.hwpf.usermodel.Section;
import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.w3c.dom.Document;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz; import org.w3c.dom.Element;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr; import org.w3c.dom.Node;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation; import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*; import java.io.*;
import java.util.Optional; import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class DocumentA4Checker { public class DocumentA4Checker {
@@ -60,7 +63,7 @@ public class DocumentA4Checker {
/** /**
* 校验 PDF 第一页是否竖版 A4 * 校验 PDF 第一页是否竖版 A4
*/ */
private static boolean checkPdfFirstPageA4(InputStream in) throws IOException { private static boolean checkPdfFirstPageA4(InputStream in) throws Exception {
try (PDDocument document = PDDocument.load(in)) { try (PDDocument document = PDDocument.load(in)) {
if (document.getNumberOfPages() == 0) return false; if (document.getNumberOfPages() == 0) return false;
@@ -76,7 +79,7 @@ public class DocumentA4Checker {
/** /**
* 校验 doc 第一页是否竖版 A4 * 校验 doc 第一页是否竖版 A4
*/ */
private static boolean checkDocFirstPageA4(InputStream in) throws IOException { private static boolean checkDocFirstPageA4(InputStream in) throws Exception {
try (HWPFDocument doc = new HWPFDocument(in)) { try (HWPFDocument doc = new HWPFDocument(in)) {
Section section = doc.getRange().getSection(0); Section section = doc.getRange().getSection(0);
@@ -92,42 +95,152 @@ public class DocumentA4Checker {
} }
/** /**
* 校验 docx 第一页是否竖版 A4POI 5.x 兼容版) * 检测 docx 中是否包含批注或修订痕迹
* 批注word/comments.xml
* 痕迹document.xml 中的 w:ins / w:del / w:delText / w:moveFrom / w:moveTo
*/ */
public static boolean isDocxA4Portrait(InputStream in) throws IOException { public static boolean hasCommentsOrTrackChanges(InputStream in) {
try (XWPFDocument docx = new XWPFDocument(in)) { try {
CTSectPr sectPr = docx.getDocument().getBody().getSectPr(); byte[] docXmlBytes = null;
if (sectPr == null || sectPr.getPgSz() == null) { boolean hasComments = false;
// 没有设置页面大小 → 默认认为是A4Word默认行为
return true;
}
CTPageSz pgSz = sectPr.getPgSz(); try (ZipInputStream zis = new ZipInputStream(in)) {
ZipEntry entry;
// 判断是否横向 while ((entry = zis.getNextEntry()) != null) {
if (pgSz.getOrient() != null) { if ("word/comments.xml".equals(entry.getName())) {
STPageOrientation.Enum orient = pgSz.getOrient(); hasComments = true;
if (STPageOrientation.LANDSCAPE.equals(orient)) { }
return false; if ("word/document.xml".equals(entry.getName())) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
while ((len = zis.read(buf)) != -1) {
bos.write(buf, 0, len);
}
docXmlBytes = bos.toByteArray();
}
} }
} }
// 兼容 BigInteger 和 BigDecimal 两种实现 if (hasComments) {
Number wTwips = (Number) pgSz.getW(); return true;
Number hTwips = (Number) pgSz.getH(); }
if (wTwips == null || hTwips == null) { // 解析 document.xml 检测修订痕迹
if (docXmlBytes != null) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document doc = factory.newDocumentBuilder()
.parse(new ByteArrayInputStream(docXmlBytes));
return hasTrackChanges(doc.getDocumentElement());
}
} catch (Exception e) {
// 读取失败不拦截
}
return false;
}
/**
* 递归检测 XML 树中是否包含修订痕迹元素
*/
private static boolean hasTrackChanges(Node node) {
if (node.getNodeType() == Node.ELEMENT_NODE) {
String name = node.getNodeName();
if ("w:ins".equals(name) || "w:del".equals(name) || "w:delText".equals(name)
|| "w:moveFrom".equals(name) || "w:moveTo".equals(name)) {
return true;
}
}
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (hasTrackChanges(children.item(i))) {
return true;
}
}
return false;
}
/**
* 校验 docx 第一页是否竖版 A4
* 直接读取 ZIP 中的 word/document.xml不依赖 POI ooxml 模块
*/
public static boolean isDocxA4Portrait(InputStream in) {
try {
byte[] docXml = extractDocXml(in);
if (docXml == null) {
return false; return false;
} }
float wPt = wTwips.floatValue() / TWIPS_TO_PT; // 不启用 namespace-aware元素名带 w: 前缀(如 w:pgSz
float hPt = hTwips.floatValue() / TWIPS_TO_PT; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document doc = factory.newDocumentBuilder()
.parse(new ByteArrayInputStream(docXml));
// 递归搜索 w:pgSz 元素sectPr 下的页面尺寸定义)
Element pgSz = findElement(doc.getDocumentElement(), "w:pgSz");
if (pgSz == null) {
return false;
}
String orient = pgSz.getAttribute("w:orient");
if ("landscape".equalsIgnoreCase(orient)) {
return false;
}
String wStr = pgSz.getAttribute("w:w");
String hStr = pgSz.getAttribute("w:h");
if (wStr == null || wStr.isEmpty() || hStr == null || hStr.isEmpty()) {
return false;
}
float wPt = Long.parseLong(wStr) / TWIPS_TO_PT;
float hPt = Long.parseLong(hStr) / TWIPS_TO_PT;
return Math.abs(wPt - A4_WIDTH) <= TOLERANCE return Math.abs(wPt - A4_WIDTH) <= TOLERANCE
&& Math.abs(hPt - A4_HEIGHT) <= TOLERANCE; && Math.abs(hPt - A4_HEIGHT) <= TOLERANCE;
} catch (Exception e) {
e.printStackTrace();
return false;
} }
} }
/**
* 从 docx ZIP 中提取 word/document.xml 的字节内容
*/
private static byte[] extractDocXml(InputStream in) throws IOException {
try (ZipInputStream zis = new ZipInputStream(in)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if ("word/document.xml".equals(entry.getName())) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
while ((len = zis.read(buf)) != -1) {
bos.write(buf, 0, len);
}
return bos.toByteArray();
}
}
}
return null;
}
/**
* 递归查找指定 tagName 的元素
*/
private static Element findElement(Node node, String tagName) {
if (node.getNodeType() == Node.ELEMENT_NODE && tagName.equals(node.getNodeName())) {
return (Element) node;
}
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Element result = findElement(children.item(i), tagName);
if (result != null) {
return result;
}
}
return null;
}
private static String getFileSuffix(String fileName) { private static String getFileSuffix(String fileName) {
int idx = fileName.lastIndexOf('.'); int idx = fileName.lastIndexOf('.');
return idx == -1 ? "" : fileName.substring(idx + 1); return idx == -1 ? "" : fileName.substring(idx + 1);