Compare commits
4 Commits
907342adb4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a941e1a8a | |||
| 98250fca47 | |||
| 717eb5fbad | |||
| 61200a3a77 |
@@ -22,6 +22,7 @@
|
||||
<bean id="esignCallbackFlowBizService" class="com.seeyon.apps.esign.service.EsignCallbackFlowBizService"/>
|
||||
<!-- 注入数组类型的Bean -->
|
||||
<bean id="esignPluginApi" class="com.seeyon.apps.esign.EsignPluginApi"/>
|
||||
<bean id="a4CheckNode" class="com.seeyon.apps.esign.node.A4CheckNode" />
|
||||
<bean name="contractCreateService" class="com.seeyon.apps.esign.service.ContractCreateService">
|
||||
<property name="signParamBuildFactories">
|
||||
<list>
|
||||
|
||||
@@ -21,7 +21,8 @@ public enum EsignConfigConstants {
|
||||
ESIGN_APP_SECRETMAP("325c8cc6c2a303d6cf1fb5657a16e591","e签宝appIDSecret映射"),
|
||||
ENABLEANTIFAKESIGN("","是否加盖防伪章"),
|
||||
FIXAPPUNIT("true","是否固定要用的E签宝应用主体单位"),
|
||||
FIXAPPUNITNAME("","固定要用的E签宝应用主体单位名称")
|
||||
FIXAPPUNITNAME("","固定要用的E签宝应用主体单位名称"),
|
||||
SIGNPARAM_LOG_URL("","E签宝参数记录路径"),
|
||||
;
|
||||
|
||||
private String defaultValue;
|
||||
|
||||
@@ -10,11 +10,11 @@ import java.util.Map;
|
||||
|
||||
public class EsignParamRecordUtils {
|
||||
|
||||
public static void saveEsignParamRecord(Map<String,Object> params) {
|
||||
public static void saveEsignParamRecord(String logFileUrl,Map<String,Object> params) {
|
||||
try {
|
||||
String str = "/home/seeyon/Seeyon/A8/ApacheJetspeed/logs_sy";
|
||||
String str = logFileUrl;
|
||||
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){
|
||||
|
||||
}
|
||||
|
||||
92
src/main/java/com/seeyon/apps/esign/node/A4CheckNode.java
Normal file
92
src/main/java/com/seeyon/apps/esign/node/A4CheckNode.java
Normal file
@@ -0,0 +1,92 @@
|
||||
package com.seeyon.apps.esign.node;
|
||||
|
||||
import com.seeyon.apps.common.workflow.node.ACommonSuperNode;
|
||||
import com.seeyon.apps.esign.constants.EsignConfigConstants;
|
||||
import com.seeyon.apps.esign.constants.FlowFormFieldConstants;
|
||||
import com.seeyon.apps.esign.utils.DocumentA4Checker;
|
||||
import com.seeyon.apps.esign.utils.FileUtil;
|
||||
import com.seeyon.apps.ext.workflow.vo.FieldDataVo;
|
||||
import com.seeyon.apps.ext.workflow.vo.FormDataVo;
|
||||
import com.seeyon.apps.ext.workflow.vo.SuperNodeContext;
|
||||
import com.seeyon.cap4.form.bean.FormDataMasterBean;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class A4CheckNode extends ACommonSuperNode {
|
||||
|
||||
private static final Log log = LogFactory.getLog(A4CheckNode.class);
|
||||
|
||||
@Override
|
||||
public String getPluginId() {
|
||||
return EsignConfigConstants.getPluginId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormParse() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuperNodeContext proceed(String s, FormDataVo formDataVo, FormDataMasterBean formDataMasterBean) throws Exception {
|
||||
SuperNodeContext context = new SuperNodeContext();
|
||||
context.setNeedSave(true);
|
||||
log.info("进入文件A4格式校验超级节点");
|
||||
try {
|
||||
FieldDataVo fieldData = formDataVo.getFieldData(FlowFormFieldConstants.CONTRACT_APPROVAL_ATTACHMENT);
|
||||
String attachmentId = fieldData.getStringValue();
|
||||
if (attachmentId == null) throw new RuntimeException("合同文件不能为空");
|
||||
String tempDir = System.getProperty("java.io.tmpdir");
|
||||
File file = null;
|
||||
FileUtil fileUtil = new FileUtil();
|
||||
List<String> paths = fileUtil.fieldFileDownload(Long.parseLong(attachmentId), tempDir + File.separator + "oafile" + File.separator);
|
||||
for (String path : paths) {
|
||||
file = new File(path);
|
||||
try {
|
||||
String fileName = file.getName().toLowerCase();
|
||||
// 检测 docx 批注/痕迹:会导致转PDF后页面撑大,不再是A4
|
||||
if (fileName.endsWith(".docx") || fileName.endsWith(".doc")) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
return context.success("文件A4格式校验通过");
|
||||
} catch (Exception e) {
|
||||
log.error("校验产生错误", e);
|
||||
e.printStackTrace();
|
||||
context.setErrMsg("校验产生错误: " + e.getMessage());
|
||||
return context.back("校验产生错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getNodeId() {
|
||||
return "nd_20260512";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNodeName() {
|
||||
return "A4格式校验节点";
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ public class EsignMultipleSignerNode extends ACommonSuperNode {
|
||||
aParty.setPartyId("甲方");
|
||||
aParty.setSignOrder(2);
|
||||
aParty.setSignerType(SignerType.ORG);
|
||||
aParty.setSealId(getStringField(formDataVo,"选择甲方印章"));
|
||||
aParty.setSealId(getStringField(formDataVo,"甲方印章ID"));
|
||||
aParty.setAutoSign(doAutoSign);
|
||||
OrgInfo orgInfo = new OrgInfo();
|
||||
aParty.setOrgInfo(orgInfo);
|
||||
|
||||
@@ -129,7 +129,7 @@ public class EsignOneSignerNode extends ACommonSuperNode {
|
||||
aParty.setSignOrder(2);
|
||||
aParty.setSignerType(SignerType.ORG);
|
||||
aParty.setAutoSign(doAutoSign);
|
||||
aParty.setSealId(getStringField(formDataVo,"选择甲方印章"));
|
||||
aParty.setSealId(getStringField(formDataVo,"甲方印章ID"));
|
||||
OrgInfo orgInfo = new OrgInfo();
|
||||
orgInfo.setOrgName(orgName);
|
||||
aParty.setOrgInfo(orgInfo);
|
||||
@@ -208,7 +208,7 @@ public class EsignOneSignerNode extends ACommonSuperNode {
|
||||
param.setPageSize(1);
|
||||
param.setOrderField("UPDATE_DATE");
|
||||
param.setSortType("DESC");
|
||||
Map<String, Object> sqlMap = FormTableExecutor.generateSql(param);
|
||||
Map<String, Object> sqlMap = SqlGenerator.generateSql(param);
|
||||
JDBCAgent agent = new JDBCAgent();
|
||||
try {
|
||||
agent.execute((String)sqlMap.get("sql"), (List)sqlMap.get("params"));
|
||||
|
||||
@@ -321,7 +321,7 @@ public class DefaultSignerBuilder implements SignerBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询文件总页数
|
||||
* 查询文件总页数,查询失败或页数为0时返回默认值10
|
||||
*/
|
||||
private Integer getFileTotalPage(String fileId, String appId) {
|
||||
try {
|
||||
@@ -336,14 +336,18 @@ public class DefaultSignerBuilder implements SignerBuilder {
|
||||
EsignBaseResp resp = JsonUtils.parseObject(respStr, EsignBaseResp.class);
|
||||
|
||||
if (!"0".equals(String.valueOf(resp.getCode()))) {
|
||||
throw new RuntimeException("查询文件上传状态失败:" + resp.getMessage());
|
||||
return 10;
|
||||
}
|
||||
|
||||
Map<String, Object> data = (Map<String, Object>) resp.getData();
|
||||
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) {
|
||||
throw new RuntimeException("获取文件页数异常", e);
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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.SignField;
|
||||
import com.seeyon.apps.esign.po.signfield.SignFieldPosition;
|
||||
@@ -13,8 +12,6 @@ import java.util.Map;
|
||||
|
||||
public class JsonSignerBuilder implements SignerBuilder{
|
||||
|
||||
private EsignConfigProvider esignConfigProvider = EsignConfigProvider.getInstance();
|
||||
|
||||
@Override
|
||||
public Signer build(List<String> fileIds, List<Object> keywordPos, SignParty party,String appId) {
|
||||
if (party.getSignerType() == SignerType.PERSON) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.seeyon.aicloud.common.JsonUtils;
|
||||
import com.seeyon.apps.esign.config.EsignConfigProvider;
|
||||
import com.seeyon.apps.esign.constants.EsignApiUrl;
|
||||
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.EsignBaseResp;
|
||||
import com.seeyon.apps.esign.utils.HttpClient;
|
||||
@@ -42,6 +43,7 @@ public class EsignByTemplateService {
|
||||
esignApiHeader.token(tokenCacheManager.getToken(appId)).appId(appId);
|
||||
String url = configProvider.getBizConfigByKey(EsignConfigConstants.ESIGN_HOST) + EsignApiUrl.SIGN_BY_FILE_URL;
|
||||
log.info("合同发起入参: " + jsonBody);
|
||||
EsignParamRecordUtils.saveEsignParamRecord(configProvider.getBizConfigByKey(EsignConfigConstants.SIGNPARAM_LOG_URL),params);
|
||||
String respStr = HttpClient.httpPostRaw(url, jsonBody, esignApiHeader.convert2Headers(), "UTF-8");
|
||||
EsignBaseResp esignBaseResp = JsonUtils.parseObject(respStr, EsignBaseResp.class);
|
||||
Map<String,Object> dataMap = (Map<String, Object>) esignBaseResp.getData();
|
||||
|
||||
248
src/main/java/com/seeyon/apps/esign/utils/DocumentA4Checker.java
Normal file
248
src/main/java/com/seeyon/apps/esign/utils/DocumentA4Checker.java
Normal file
@@ -0,0 +1,248 @@
|
||||
package com.seeyon.apps.esign.utils;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.poi.hwpf.HWPFDocument;
|
||||
import org.apache.poi.hwpf.usermodel.Section;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import java.io.*;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public class DocumentA4Checker {
|
||||
|
||||
// A4标准磅值 宽595 高842,允许误差5pt
|
||||
private static final float A4_WIDTH = 595.0f;
|
||||
private static final float A4_HEIGHT = 842.0f;
|
||||
private static final float TOLERANCE = 5.0f;
|
||||
// twips 转 pt: /20
|
||||
private static final float TWIPS_TO_PT = 20.0f;
|
||||
|
||||
public static boolean isFirstPageA4(File file) {
|
||||
if (file == null || !file.exists() || !file.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String fileName = Optional.ofNullable(file.getName()).orElse("");
|
||||
String suffix = getFileSuffix(fileName).toLowerCase();
|
||||
|
||||
try {
|
||||
switch (suffix) {
|
||||
case "pdf":
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
return checkPdfFirstPageA4(in);
|
||||
}
|
||||
case "doc":
|
||||
// 先尝试 doc 解析,若文件实际为 OOXML 则回退到 docx
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
return checkDocFirstPageA4(in);
|
||||
} catch (IllegalArgumentException e) {
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
return isDocxA4Portrait(in);
|
||||
}
|
||||
}
|
||||
case "docx":
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
return isDocxA4Portrait(in);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 PDF 第一页是否竖版 A4
|
||||
*/
|
||||
private static boolean checkPdfFirstPageA4(InputStream in) throws Exception {
|
||||
try (PDDocument document = PDDocument.load(in)) {
|
||||
if (document.getNumberOfPages() == 0) return false;
|
||||
|
||||
PDPage page = document.getPage(0);
|
||||
float w = page.getMediaBox().getWidth();
|
||||
float h = page.getMediaBox().getHeight();
|
||||
|
||||
return Math.abs(w - A4_WIDTH) <= TOLERANCE
|
||||
&& Math.abs(h - A4_HEIGHT) <= TOLERANCE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 doc 第一页是否竖版 A4
|
||||
*/
|
||||
private static boolean checkDocFirstPageA4(InputStream in) throws Exception {
|
||||
try (HWPFDocument doc = new HWPFDocument(in)) {
|
||||
Section section = doc.getRange().getSection(0);
|
||||
|
||||
int widthTwips = section.getPageWidth();
|
||||
int heightTwips = section.getPageHeight();
|
||||
|
||||
float widthPt = widthTwips / 20f;
|
||||
float heightPt = heightTwips / 20f;
|
||||
|
||||
return Math.abs(widthPt - A4_WIDTH) <= TOLERANCE
|
||||
&& Math.abs(heightPt - A4_HEIGHT) <= TOLERANCE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测 docx 中是否包含批注或修订痕迹
|
||||
* 批注:word/comments.xml
|
||||
* 痕迹:document.xml 中的 w:ins / w:del / w:delText / w:moveFrom / w:moveTo
|
||||
*/
|
||||
public static boolean hasCommentsOrTrackChanges(InputStream in) {
|
||||
try {
|
||||
byte[] docXmlBytes = null;
|
||||
boolean hasComments = false;
|
||||
|
||||
try (ZipInputStream zis = new ZipInputStream(in)) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
if ("word/comments.xml".equals(entry.getName())) {
|
||||
hasComments = true;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasComments) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 解析 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;
|
||||
}
|
||||
|
||||
// 不启用 namespace-aware,元素名带 w: 前缀(如 w:pgSz)
|
||||
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
|
||||
&& 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) {
|
||||
int idx = fileName.lastIndexOf('.');
|
||||
return idx == -1 ? "" : fileName.substring(idx + 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user