|
|
@@ -4,9 +4,10 @@ import cn.hutool.core.util.ObjectUtil;
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
import com.alibaba.fastjson.JSONArray;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.malk.sdzl.common.EqbYidaFormConfig;
|
|
|
+import com.malk.sdzl.config.EqbAccountProperties;
|
|
|
import com.malk.sdzl.service.EqbService;
|
|
|
import com.malk.sdzl.util.HTTPHelper;
|
|
|
-import com.malk.sdzl.service.EqbService;
|
|
|
import com.malk.server.aliwork.YDConf;
|
|
|
import com.malk.server.aliwork.YDParam;
|
|
|
import com.malk.server.common.McR;
|
|
|
@@ -53,25 +54,11 @@ public class EqbServiceImpl implements EqbService {
|
|
|
// 按接口文档每3秒轮询一次,文件状态查询及等待时间合计最多90秒。
|
|
|
private static final long FILE_STATUS_POLL_INTERVAL_MILLIS = 3000;
|
|
|
private static final long FILE_STATUS_POLL_TIMEOUT_MILLIS = 90000;
|
|
|
- private static final String TEMPORARY_URL_ATTACHMENT_FIELD = "attachmentField_mrkcwgh7";
|
|
|
- private static final String DIRECT_URL_ATTACHMENT_FIELD = "attachmentField_mr1tqpgz";
|
|
|
private static final List<String> FORM_SIGNER_PARTIES =
|
|
|
Collections.unmodifiableList(Arrays.asList("甲方", "乙方"));
|
|
|
private static final String OUR_PARTY = "乙方";
|
|
|
private static final String PARTY_A_SIGN_KEYWORD = "甲方/甲方授权代表电子签名";
|
|
|
private static final String PARTY_B_SIGN_KEYWORD = "乙方电子签名";
|
|
|
- /** 未配置主体或印章类型时使用的默认印章。 */
|
|
|
- private static final String DEFAULT_OUR_PARTY_SEAL_ID = "25b69e0c-4d74-4c8d-9164-eb10891a284d";
|
|
|
- /** 宜搭表单中的考核主体字段。 */
|
|
|
- private static final String ASSESSMENT_SUBJECT_FIELD_ID = "textField_asr5cxt";
|
|
|
- /** 宜搭表单中的核算印章类型字段。 */
|
|
|
- private static final String ACCOUNTING_SEAL_TYPE_FIELD_ID = "textField_3mf3de6";
|
|
|
- /**
|
|
|
- * 考核主体与印章类型对应的 e 签宝印章 ID。
|
|
|
- * 后续新增主体时,在该映射中增加一个主体及其印章类型配置即可。
|
|
|
- */
|
|
|
- private static final Map<String, Map<String, String>> ASSESSMENT_SUBJECT_SEAL_IDS =
|
|
|
- createAssessmentSubjectSealIds();
|
|
|
private static final String INTERNAL_SIGN_KEYWORD = "_signKeyword";
|
|
|
private static final Set<String> UPLOAD_FILE_EXTENSIONS = new HashSet<>(Arrays.asList(
|
|
|
"pdf", "docx", "doc", "rtf", "xlsx", "xls", "pptx", "ppt", "wps", "et", "dps",
|
|
|
@@ -116,6 +103,10 @@ public class EqbServiceImpl implements EqbService {
|
|
|
@Value("${eqb.apiHost}")
|
|
|
private String eqbApiHost;
|
|
|
|
|
|
+ // e签宝签署流程完结通知的公网地址;配置后自动写入发起签署请求。
|
|
|
+ @Value("${eqb.callbackUrl:}")
|
|
|
+ private String eqbCallbackUrl;
|
|
|
+
|
|
|
// @Value(value = "${server.host}")
|
|
|
private String serverHost;
|
|
|
|
|
|
@@ -128,19 +119,10 @@ public class EqbServiceImpl implements EqbService {
|
|
|
@Autowired
|
|
|
private YDService ydService;
|
|
|
|
|
|
- private volatile List<Map> MATE;
|
|
|
-
|
|
|
- private static Map<String, Map<String, String>> createAssessmentSubjectSealIds() {
|
|
|
- Map<String, String> tianJiSealIds = new LinkedHashMap<>();
|
|
|
- tianJiSealIds.put("公章", "4b8cd76c-d0e4-4874-badb-0df0a974abf1");
|
|
|
- tianJiSealIds.put("合同专用章", "7a51d006-bb92-4b5b-86c6-4f639ee94a40");
|
|
|
- tianJiSealIds.put("合同章", "7a51d006-bb92-4b5b-86c6-4f639ee94a40");
|
|
|
+ @Autowired
|
|
|
+ private EqbAccountProperties eqbAccountProperties;
|
|
|
|
|
|
- Map<String, Map<String, String>> subjectSealIds = new LinkedHashMap<>();
|
|
|
- subjectSealIds.put("上海添玑好房网络服务有限公司",
|
|
|
- Collections.unmodifiableMap(tianJiSealIds));
|
|
|
- return Collections.unmodifiableMap(subjectSealIds);
|
|
|
- }
|
|
|
+ private volatile List<Map> MATE;
|
|
|
|
|
|
/**
|
|
|
* 加载json
|
|
|
@@ -184,13 +166,14 @@ public class EqbServiceImpl implements EqbService {
|
|
|
throw new IllegalArgumentException("formUuid不能为空,必须通过请求参数传入");
|
|
|
}
|
|
|
String formUuid = ((String) rawUuid).trim();
|
|
|
+ EqbYidaFormConfig formConfig = EqbYidaFormConfig.require(formUuid);
|
|
|
resultData.put("formInstance", formInstance);
|
|
|
+ resultData.put("formUuid", formUuid);
|
|
|
+ resultData.put("writeBackAttachmentFieldId",
|
|
|
+ formConfig.getWriteBackAttachmentFieldId());
|
|
|
if (!hasText(appType) || !hasText(systemToken)) {
|
|
|
throw new IllegalStateException("未配置aliwork.appType或aliwork.systemToken");
|
|
|
}
|
|
|
- if (!hasText(downloadFilePath)) {
|
|
|
- throw new IllegalStateException("未配置eqb.downloadFilePath");
|
|
|
- }
|
|
|
DDR_New form = ydClient.queryData(YDParam.builder()
|
|
|
.appType(appType)
|
|
|
.systemToken(systemToken)
|
|
|
@@ -200,17 +183,33 @@ public class EqbServiceImpl implements EqbService {
|
|
|
if (form == null || form.getFormData() == null) {
|
|
|
throw new IllegalStateException("查询宜搭表单失败或未返回表单数据");
|
|
|
}
|
|
|
+ Map formData = form.getFormData();
|
|
|
+ String electronicSign = trimToEmpty(
|
|
|
+ UtilMap.getString(formData, formConfig.getElectronicSignFieldId()));
|
|
|
+ resultData.put("electronicSign", electronicSign);
|
|
|
+ if (!shouldStartElectronicSign(formUuid, formData)) {
|
|
|
+ resultData.put("status", "SKIPPED");
|
|
|
+ resultData.put("reason", "是否发起电子签不是“是”,未发起签署");
|
|
|
+ return McR.success(resultData);
|
|
|
+ }
|
|
|
+ String ourCompany = requireFormText(formData,
|
|
|
+ formConfig.getOurCompanyFieldId(), "我方签约公司");
|
|
|
+ EqbAccountProperties.Account eqbAccount =
|
|
|
+ eqbAccountProperties.requireByCompany(ourCompany);
|
|
|
+ resultData.put("eqbAppId", eqbAccount.getAppId());
|
|
|
+ if (!hasText(downloadFilePath)) {
|
|
|
+ throw new IllegalStateException("未配置eqb.downloadFilePath");
|
|
|
+ }
|
|
|
List<FormAttachment> attachments = new ArrayList<>();
|
|
|
-// collectAttachments(form.getFormData(), TEMPORARY_URL_ATTACHMENT_FIELD, attachments);
|
|
|
- collectAttachments(form.getFormData(), DIRECT_URL_ATTACHMENT_FIELD, attachments);
|
|
|
+ collectAttachments(formData, formConfig.getContractAttachmentFieldId(), attachments);
|
|
|
if (attachments.isEmpty()) {
|
|
|
- throw new IllegalArgumentException("附件组件中均没有可上传的文件");
|
|
|
+ throw new IllegalArgumentException("合同附件中没有可上传的文件");
|
|
|
}
|
|
|
- stage = "查询相对方登记信息";
|
|
|
- // 表单模式不采用请求中的signers,所有参与方均由表单编号及相对方登记信息生成。
|
|
|
+ stage = "构建签署方信息";
|
|
|
+ // 表单模式不采用请求中的signers;我方固定为乙方,对方固定为甲方。
|
|
|
Map<String, Object> signParams = new HashMap<>(params);
|
|
|
signParams.remove("autoSignKeywords");
|
|
|
- signParams.put("signers", buildFormSigners(formUuid, form.getFormData()));
|
|
|
+ signParams.put("signers", buildFormSigners(formUuid, formData));
|
|
|
// 整个表单共用签署配置;未指定主题时使用第一个附件名,并校验展开后的签署区总数。
|
|
|
stage = "校验签署配置";
|
|
|
JSONObject signRequest = prepareSignRequest(signParams, attachments.get(0).fileName, attachments.size());
|
|
|
@@ -244,7 +243,7 @@ public class EqbServiceImpl implements EqbService {
|
|
|
stage = "下载附件:" + attachment.fileName;
|
|
|
String downloadUrl = attachment.downloadUrl;
|
|
|
|
|
|
- if (!DIRECT_URL_ATTACHMENT_FIELD.equals(attachment.fieldId)) {
|
|
|
+ if (!"attachmentField_mq625xl3".equals(attachment.fieldId)) {
|
|
|
// 宜搭原生附件需先换取临时免登地址,第二个组件直接使用其下载地址。
|
|
|
downloadUrl = ydClient.convertTemporaryUrl(downloadUrl, appType, systemToken);
|
|
|
if (!hasText(downloadUrl)) {
|
|
|
@@ -263,7 +262,7 @@ public class EqbServiceImpl implements EqbService {
|
|
|
for (int i = 0; i < attachments.size(); i++) {
|
|
|
FormAttachment attachment = attachments.get(i);
|
|
|
stage = "上传并确认附件状态:" + attachment.fileName;
|
|
|
- McR result = uploadLocalFile(uploadParams.get(i), attachment.localFile);
|
|
|
+ McR result = uploadLocalFile(uploadParams.get(i), attachment.localFile, eqbAccount);
|
|
|
Map<String, Object> fileResult = fileResults.get(i);
|
|
|
fileResult.put("success", result.isSuccess());
|
|
|
fileResult.put("code", result.getCode());
|
|
|
@@ -280,21 +279,15 @@ public class EqbServiceImpl implements EqbService {
|
|
|
}
|
|
|
// 所有附件均已达到可签署状态后,仅调用一次创建接口,共享同一个signFlowId。
|
|
|
stage = "创建表单附件签署流程";
|
|
|
- String signFlowId = createUploadedFilesSignFlow(signRequest, fileResults);
|
|
|
+ String signFlowId = createUploadedFilesSignFlow(signRequest, fileResults, eqbAccount);
|
|
|
resultData.put("signFlowId", signFlowId);
|
|
|
for (Map<String, Object> fileResult : fileResults) {
|
|
|
fileResult.put("signFlowId", signFlowId);
|
|
|
fileResult.put("status", "SIGN_FLOW_CREATED");
|
|
|
}
|
|
|
-// ydClient.operateData(YDParam.builder()
|
|
|
-// .appType(appType)
|
|
|
-// .systemToken(systemToken)
|
|
|
-//// .formInstanceId(request.getInstanceId())
|
|
|
-// .formUuid("FORM-3C73F8BB960244DE841E2DFD6B0A2818V7H5")
|
|
|
-//// .useLatestVersion(true)
|
|
|
-// .formDataJson(JSON.toJSONString(UtilMap.map("textField_mte3s2rf, textField_mte6b6fs, radioField_mte6b6fw",
|
|
|
-// signFlowId, formInstance, "否")))
|
|
|
-// .build(), YDConf.FORM_OPERATION.create);
|
|
|
+ stage = "创建签署跟踪记录";
|
|
|
+ createSignFlowTracking(formUuid, formInstance, signFlowId);
|
|
|
+ resultData.put("status", "SIGN_FLOW_CREATED");
|
|
|
return McR.success(resultData);
|
|
|
} catch (Exception e) {
|
|
|
LOG.error("宜搭附件处理失败,阶段:{}", stage, e);
|
|
|
@@ -305,6 +298,39 @@ public class EqbServiceImpl implements EqbService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 保存签署流程与原业务表单的关系,供异步回调选择正确的附件回写字段。 */
|
|
|
+ private void createSignFlowTracking(String formUuid, String formInstanceId,
|
|
|
+ String signFlowId) {
|
|
|
+ Map<String, Object> trackingData = buildSignFlowTrackingData(
|
|
|
+ formUuid, formInstanceId, signFlowId);
|
|
|
+ ydClient.operateData(YDParam.builder()
|
|
|
+ .appType(appType)
|
|
|
+ .systemToken(systemToken)
|
|
|
+ .formUuid(EqbYidaFormConfig.SIGN_FLOW_TRACKING_FORM_UUID)
|
|
|
+ .formDataJson(JSON.toJSONString(trackingData))
|
|
|
+ .build(), YDConf.FORM_OPERATION.create);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 构建签署跟踪表数据,formUuid和流程实例ID分别写入独立字段。 */
|
|
|
+ static Map<String, Object> buildSignFlowTrackingData(
|
|
|
+ String formUuid, String formInstanceId, String signFlowId) {
|
|
|
+ EqbYidaFormConfig formConfig = EqbYidaFormConfig.require(formUuid);
|
|
|
+ if (formInstanceId == null || formInstanceId.trim().isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("流程实例ID不能为空");
|
|
|
+ }
|
|
|
+ if (signFlowId == null || signFlowId.trim().isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("e签宝签署流程ID不能为空");
|
|
|
+ }
|
|
|
+ Map<String, Object> trackingData = new LinkedHashMap<>();
|
|
|
+ trackingData.put(EqbYidaFormConfig.TRACKING_SIGN_FLOW_ID_FIELD, signFlowId.trim());
|
|
|
+ trackingData.put(EqbYidaFormConfig.TRACKING_FORM_UUID_FIELD,
|
|
|
+ formConfig.getFormUuid());
|
|
|
+ trackingData.put(EqbYidaFormConfig.TRACKING_FORM_INSTANCE_ID_FIELD,
|
|
|
+ formInstanceId.trim());
|
|
|
+ trackingData.put(EqbYidaFormConfig.TRACKING_HANDLED_FIELD, "否");
|
|
|
+ return trackingData;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 使用请求中的formUuid匹配party-fields.json,再按甲、乙顺序提取编号。
|
|
|
* 未配置或未填写的参与方跳过;formData仅提供字段值,不读取其中的表单UUID。
|
|
|
@@ -386,94 +412,95 @@ public class EqbServiceImpl implements EqbService {
|
|
|
return partyNumbers;
|
|
|
}
|
|
|
|
|
|
- /** 每个有值的参与方独立查询一次登记表,并转换为e签宝签署方信息。 */
|
|
|
+ /**
|
|
|
+ * 根据当前 formUuid 的字段配置构建签署方。我方固定为乙方并自动落印,
|
|
|
+ * 对方固定为甲方并使用表单中的接收人姓名、电话完成手动签署。
|
|
|
+ */
|
|
|
protected JSONArray buildFormSigners(String formUuid, Map formData) {
|
|
|
-
|
|
|
- JSONArray signers = new JSONArray();
|
|
|
- String assignedSealId = resolveAssignedSealId(formData);
|
|
|
- String jName = UtilMap.getString(formData,"textField_e6og3c9");
|
|
|
- String jPhone = UtilMap.getString(formData,"textField_ismg45u");
|
|
|
- String yName = UtilMap.getString(formData,"textField_mtwp487i");
|
|
|
- String yPhone = UtilMap.getString(formData,"textField_mtwp487h");
|
|
|
- for (Map<String, String> party : extractFormPartyNumbers(formUuid, formData)) {
|
|
|
- String partyName = party.get("party");
|
|
|
- boolean isOurParty = OUR_PARTY.equals(partyName);
|
|
|
- String signerName = "甲方".equals(partyName) ? jName : yName;
|
|
|
- String signerPhone = "甲方".equals(partyName) ? jPhone : yPhone;
|
|
|
-// List<Map> dataList = ydService.queryFormData_all(YDParam.builder()
|
|
|
-// .appType(appType)
|
|
|
-// .systemToken(systemToken)
|
|
|
-// .formUuid("FORM-B7DBC8CA2A284FDC9F38E814CC043B17DIMN")
|
|
|
-// .searchFieldJson(JSON.toJSONString(UtilMap.map("serialNumberField_mrlipwdo", party.get("partyNumber"))))
|
|
|
-// .build());
|
|
|
-// // 同一编号不能任取一条,避免登记信息缺失或重复时绑定错误签署主体。
|
|
|
-// if (dataList == null) {
|
|
|
-// throw new IllegalStateException(partyName + "相对方登记查询未返回数据");
|
|
|
-// }
|
|
|
-// if (dataList.isEmpty()) {
|
|
|
-// throw new IllegalArgumentException(partyName + "编号未查到相对方登记记录");
|
|
|
-// }
|
|
|
-// if (dataList.size() != 1 || dataList.get(0) == null) {
|
|
|
-// throw new IllegalArgumentException(partyName + "编号必须对应唯一一条相对方登记记录");
|
|
|
-// }
|
|
|
-// Map data = dataList.get(0);
|
|
|
-// String type = requirePartyText(data, "radioField_ms4kbtdz", partyName, "相对方类型");
|
|
|
-// String name = requirePartyText(data, "textField_mrlipwdn", partyName, "名称");
|
|
|
-// String idCardNum = requirePartyText(data, "textField_mrlipwdt", partyName, "证件号码");
|
|
|
-// String legalPerson = requirePartyText(data, "textField_ms4js5qb", partyName, "法人代表");
|
|
|
- JSONObject signer = new JSONObject();
|
|
|
- if ("甲方".equals(partyName)) {
|
|
|
- signer.put("signerType", 0);
|
|
|
- JSONObject psnInfo = new JSONObject();
|
|
|
-// psnInfo.put("psnName", name);
|
|
|
-// psnInfo.put("psnIDCardNum", idCardNum);
|
|
|
-// psnInfo.put("psnIDCardType", "护照".equals(data.get("radioField_ms4js5q6"))
|
|
|
-// ? "CRED_PSN_PASSPORT" : "CRED_PSN_CH_IDCARD");
|
|
|
- JSONObject psnSignerInfo = new JSONObject();
|
|
|
- psnSignerInfo.put("psnInfo", psnInfo);
|
|
|
- psnInfo.put("psnName", signerName);
|
|
|
- psnSignerInfo.put("psnAccount", signerPhone);
|
|
|
- signer.put("psnSignerInfo", psnSignerInfo);
|
|
|
- } else {
|
|
|
- signer.put("signerType", 1);
|
|
|
- JSONObject orgInfo = new JSONObject();
|
|
|
- orgInfo.put("orgIDCardType", "CRED_ORG_USCC");
|
|
|
- orgInfo.put("orgIDCardNum", "91310106MAEC5CTQ0A");
|
|
|
- JSONObject orgSignerInfo = new JSONObject();
|
|
|
- orgSignerInfo.put("orgName", "上海深度智联科技有限公司");
|
|
|
- orgSignerInfo.put("orgInfo", orgInfo);
|
|
|
- if (!isOurParty) {
|
|
|
- JSONObject psnInfo = new JSONObject();
|
|
|
- psnInfo.put("psnName", signerName);
|
|
|
- JSONObject transactorInfo = new JSONObject();
|
|
|
- transactorInfo.put("psnAccount", signerPhone);
|
|
|
- transactorInfo.put("psnInfo", psnInfo);
|
|
|
- orgSignerInfo.put("transactorInfo", transactorInfo);
|
|
|
- signer.put("orgSignerInfo", orgSignerInfo);
|
|
|
- }
|
|
|
- }
|
|
|
- signer.put("signFields", Collections.singletonList(
|
|
|
- createSignField(partyName, assignedSealId)));
|
|
|
- signers.add(signer);
|
|
|
+ if (formData == null) {
|
|
|
+ throw new IllegalArgumentException("宜搭表单数据不能为空");
|
|
|
}
|
|
|
+ EqbYidaFormConfig formConfig = EqbYidaFormConfig.require(formUuid);
|
|
|
+ String counterpartyName = requireFormText(formData,
|
|
|
+ formConfig.getCounterpartyRecipientFieldId(), "对方接收人");
|
|
|
+ String counterpartyPhone = requireFormText(formData,
|
|
|
+ formConfig.getCounterpartyPhoneFieldId(), "对方接收电话");
|
|
|
+ String ourCompany = requireFormText(formData,
|
|
|
+ formConfig.getOurCompanyFieldId(), "我方签约公司");
|
|
|
+ String sealType = trimToEmpty(
|
|
|
+ UtilMap.getString(formData, formConfig.getSealTypeFieldId()));
|
|
|
+ String assignedSealId = resolveAssignedSealId(ourCompany, sealType);
|
|
|
+ JSONArray signers = new JSONArray();
|
|
|
+
|
|
|
+ JSONObject counterpartySigner = new JSONObject();
|
|
|
+ counterpartySigner.put("signerType", 0);
|
|
|
+ JSONObject counterpartyPsnInfo = new JSONObject();
|
|
|
+ counterpartyPsnInfo.put("psnName", counterpartyName);
|
|
|
+ JSONObject counterpartySignerInfo = new JSONObject();
|
|
|
+ counterpartySignerInfo.put("psnInfo", counterpartyPsnInfo);
|
|
|
+ counterpartySignerInfo.put("psnAccount", counterpartyPhone);
|
|
|
+ counterpartySigner.put("psnSignerInfo", counterpartySignerInfo);
|
|
|
+ counterpartySigner.put("signFields", Collections.singletonList(
|
|
|
+ createSignField("甲方", assignedSealId)));
|
|
|
+ signers.add(counterpartySigner);
|
|
|
+
|
|
|
+ JSONObject ourSigner = new JSONObject();
|
|
|
+ ourSigner.put("signerType", 1);
|
|
|
+ // 机构自动签署时不传orgSignerInfo,由当前appId对应的机构主体完成自动落印。
|
|
|
+ ourSigner.put("signFields", Collections.singletonList(
|
|
|
+ createSignField(OUR_PARTY, assignedSealId)));
|
|
|
+ signers.add(ourSigner);
|
|
|
return signers;
|
|
|
}
|
|
|
|
|
|
- /** 根据考核主体和核算印章类型选择印章;未配置的组合统一使用默认印章。 */
|
|
|
- private String resolveAssignedSealId(Map formData) {
|
|
|
- if (formData == null) {
|
|
|
- return DEFAULT_OUR_PARTY_SEAL_ID;
|
|
|
+ /**
|
|
|
+ * 按我方签约公司名称查询e签宝机构认证信息,并返回自动签署要求的机构orgId。
|
|
|
+ * 查询和后续签署使用同一个主体账号,防止多主体场景下混用appId、appKey。
|
|
|
+ *
|
|
|
+ * @see <a href="https://open.esign.cn/doc/opendoc/auth3/xxz4tc">查询机构认证信息</a>
|
|
|
+ */
|
|
|
+ protected String queryOrganizationId(String orgName,
|
|
|
+ EqbAccountProperties.Account eqbAccount) {
|
|
|
+ if (!hasText(orgName)) {
|
|
|
+ throw new IllegalArgumentException("我方签约公司不能为空");
|
|
|
}
|
|
|
- String assessmentSubject = trimToEmpty(
|
|
|
- UtilMap.getString(formData, ASSESSMENT_SUBJECT_FIELD_ID));
|
|
|
- String sealType = trimToEmpty(
|
|
|
- UtilMap.getString(formData, ACCOUNTING_SEAL_TYPE_FIELD_ID));
|
|
|
- Map<String, String> sealIdsByType = ASSESSMENT_SUBJECT_SEAL_IDS.get(assessmentSubject);
|
|
|
- if (sealIdsByType == null) {
|
|
|
- return DEFAULT_OUR_PARTY_SEAL_ID;
|
|
|
+ if (eqbAccount == null) {
|
|
|
+ throw new IllegalArgumentException("e签宝主体账号不能为空");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String normalizedOrgName = orgName.trim();
|
|
|
+ String encodedOrgName = URLEncoder.encode(normalizedOrgName, "UTF-8")
|
|
|
+ .replace("+", "%20");
|
|
|
+ String apiPath = "/v3/organizations/identity-info?orgName=";
|
|
|
+ // 鉴权文档要求Query原值参与签名;仅在发送含中文的HTTP URL时进行UrlEncode。
|
|
|
+ String signaturePath = apiPath + normalizedOrgName;
|
|
|
+ String requestPath = apiPath + encodedOrgName;
|
|
|
+ Map organization = requestEqbApi(
|
|
|
+ "GET", requestPath, signaturePath, null, 60000, eqbAccount);
|
|
|
+ Object rawOrgId = organization == null ? null : organization.get("orgId");
|
|
|
+ if (!(rawOrgId instanceof String) || !hasText((String) rawOrgId)) {
|
|
|
+ throw new IllegalStateException("查询机构认证信息未返回orgId");
|
|
|
+ }
|
|
|
+ return ((String) rawOrgId).trim();
|
|
|
+ } catch (UnsupportedEncodingException e) {
|
|
|
+ throw new IllegalStateException("UTF-8编码不可用", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 是否发起电子签字段只有明确为“是”时才继续创建签署流程。 */
|
|
|
+ protected boolean shouldStartElectronicSign(String formUuid, Map formData) {
|
|
|
+ if (formData == null) {
|
|
|
+ return false;
|
|
|
}
|
|
|
- String assignedSealId = sealIdsByType.get(sealType);
|
|
|
- return hasText(assignedSealId) ? assignedSealId : DEFAULT_OUR_PARTY_SEAL_ID;
|
|
|
+ EqbYidaFormConfig formConfig = EqbYidaFormConfig.require(formUuid);
|
|
|
+ String electronicSign = trimToEmpty(
|
|
|
+ UtilMap.getString(formData, formConfig.getElectronicSignFieldId()));
|
|
|
+ return "是".equals(electronicSign);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 根据我方签约公司和印章类型,从当前环境配置中选择自动落印的印章ID。 */
|
|
|
+ private String resolveAssignedSealId(String ourCompany, String sealType) {
|
|
|
+ return eqbAccountProperties.requireSealId(ourCompany, sealType);
|
|
|
}
|
|
|
|
|
|
private String trimToEmpty(String value) {
|
|
|
@@ -487,8 +514,10 @@ public class EqbServiceImpl implements EqbService {
|
|
|
fieldConfig.put("freeMode", false);
|
|
|
fieldConfig.put("signFieldStyle", 1);
|
|
|
if (isOurParty) {
|
|
|
- fieldConfig.put("assignedSealId", hasText(assignedSealId)
|
|
|
- ? assignedSealId : DEFAULT_OUR_PARTY_SEAL_ID);
|
|
|
+ if (!hasText(assignedSealId)) {
|
|
|
+ throw new IllegalArgumentException("自动落印的印章ID不能为空");
|
|
|
+ }
|
|
|
+ fieldConfig.put("assignedSealId", assignedSealId.trim());
|
|
|
}
|
|
|
JSONObject field = new JSONObject();
|
|
|
field.put("normalSignFieldConfig", fieldConfig);
|
|
|
@@ -506,10 +535,10 @@ public class EqbServiceImpl implements EqbService {
|
|
|
throw new IllegalArgumentException("不支持的签署方:" + partyName);
|
|
|
}
|
|
|
|
|
|
- private String requirePartyText(Map data, String fieldId, String partyName, String label) {
|
|
|
+ private String requireFormText(Map data, String fieldId, String label) {
|
|
|
Object value = data.get(fieldId);
|
|
|
if (!(value instanceof String) || !hasText((String) value)) {
|
|
|
- throw new IllegalArgumentException(partyName + "相对方登记缺少有效的" + label + ",字段:" + fieldId);
|
|
|
+ throw new IllegalArgumentException(label + "不能为空,字段:" + fieldId);
|
|
|
}
|
|
|
return ((String) value).trim();
|
|
|
}
|
|
|
@@ -651,7 +680,8 @@ public class EqbServiceImpl implements EqbService {
|
|
|
}
|
|
|
|
|
|
/** 只上传并等待文件就绪,不创建签署流程;downloadedFile仅供表单下载流程内部传入。 */
|
|
|
- private McR uploadLocalFile(Map<String, Object> params, File downloadedFile) {
|
|
|
+ private McR uploadLocalFile(Map<String, Object> params, File downloadedFile,
|
|
|
+ EqbAccountProperties.Account eqbAccount) {
|
|
|
try {
|
|
|
// 签署参数由调用方统一校验,此处校验本地文件与转换选项。
|
|
|
Object name = params == null ? null : params.get("fileName");
|
|
|
@@ -692,7 +722,7 @@ public class EqbServiceImpl implements EqbService {
|
|
|
request.put("convertToOFD", convertToOFD);
|
|
|
|
|
|
// 获取fileId仅代表预分配文件记录,此时文件尚未上传,不能直接用于签署。
|
|
|
- Map uploadInfo = getFileUploadUrl(request);
|
|
|
+ Map uploadInfo = getFileUploadUrl(request, eqbAccount);
|
|
|
String fileId = uploadInfo == null ? null : UtilMap.getString(uploadInfo, "fileId");
|
|
|
String fileUploadUrl = uploadInfo == null ? null : UtilMap.getString(uploadInfo, "fileUploadUrl");
|
|
|
if (fileId == null || fileId.trim().isEmpty()
|
|
|
@@ -701,7 +731,7 @@ public class EqbServiceImpl implements EqbService {
|
|
|
}
|
|
|
// PUT内部同时校验HTTP状态和errCode;上传接口成功后仍需等待服务端异步处理。
|
|
|
uploadFileStream(file, fileSize, fileUploadUrl, contentMd5, contentType);
|
|
|
- int fileStatus = waitForUploadedFile(fileId);
|
|
|
+ int fileStatus = waitForUploadedFile(fileId, eqbAccount);
|
|
|
|
|
|
Map<String, Object> result = new HashMap<>();
|
|
|
result.put("fileId", fileId);
|
|
|
@@ -725,6 +755,12 @@ public class EqbServiceImpl implements EqbService {
|
|
|
|
|
|
/** 将全部已上传文件绑定到同一个流程,每位签署方的签署区配置应用于每个附件。 */
|
|
|
private String createUploadedFilesSignFlow(JSONObject signRequest, List<Map<String, Object>> files) {
|
|
|
+ return createUploadedFilesSignFlow(signRequest, files, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String createUploadedFilesSignFlow(JSONObject signRequest,
|
|
|
+ List<Map<String, Object>> files,
|
|
|
+ EqbAccountProperties.Account eqbAccount) {
|
|
|
List<Map<String, Object>> docs = new ArrayList<>();
|
|
|
for (Map<String, Object> file : files) {
|
|
|
// 文件转换后可能改变扩展名,省略docs.fileName以保留服务端文件名。
|
|
|
@@ -748,8 +784,8 @@ public class EqbServiceImpl implements EqbService {
|
|
|
signer.put("signFields", fields);
|
|
|
}
|
|
|
// fileId 只有在所有附件上传完成后才可确定,因此自动落印的关键字定位必须在此处执行。
|
|
|
- applySignKeywordPositions(signRequest);
|
|
|
- Map signResult = createSignFlow(signRequest);
|
|
|
+ applySignKeywordPositions(signRequest, eqbAccount);
|
|
|
+ Map signResult = createSignFlow(signRequest, eqbAccount);
|
|
|
Object signFlowId = signResult == null ? null : signResult.get("signFlowId");
|
|
|
if (!(signFlowId instanceof String) || !hasText((String) signFlowId)) {
|
|
|
throw new IllegalStateException("发起签署未返回signFlowId");
|
|
|
@@ -791,8 +827,12 @@ public class EqbServiceImpl implements EqbService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** 根据已绑定的关键字定位每个展开后的签署区,并将坐标写回signFieldPosition。 */
|
|
|
- private void applySignKeywordPositions(JSONObject signRequest) {
|
|
|
+ /**
|
|
|
+ * 根据已绑定的关键字定位签署区;同一关键字出现多次时,每个坐标都生成独立签署区。
|
|
|
+ * Excel多Sheet转换为多页后,可确保每个Sheet中的相同关键字都参与签署。
|
|
|
+ */
|
|
|
+ private void applySignKeywordPositions(JSONObject signRequest,
|
|
|
+ EqbAccountProperties.Account eqbAccount) {
|
|
|
Map<String, Set<String>> keywordsByFile = new LinkedHashMap<>();
|
|
|
for (Object signerValue : signRequest.getJSONArray("signers")) {
|
|
|
JSONObject signer = (JSONObject) signerValue;
|
|
|
@@ -814,22 +854,26 @@ public class EqbServiceImpl implements EqbService {
|
|
|
Map<String, List<KeywordPosition>> positionsByFile = new HashMap<>();
|
|
|
for (Map.Entry<String, Set<String>> entry : keywordsByFile.entrySet()) {
|
|
|
positionsByFile.put(entry.getKey(),
|
|
|
- queryKeywordPositions(entry.getKey(), new ArrayList<>(entry.getValue())));
|
|
|
+ queryKeywordPositions(entry.getKey(), new ArrayList<>(entry.getValue()), eqbAccount));
|
|
|
}
|
|
|
|
|
|
+ int expandedFieldCount = 0;
|
|
|
for (Object signerValue : signRequest.getJSONArray("signers")) {
|
|
|
JSONObject signer = (JSONObject) signerValue;
|
|
|
+ JSONArray expandedFields = new JSONArray();
|
|
|
for (Object fieldValue : signer.getJSONArray("signFields")) {
|
|
|
JSONObject signField = (JSONObject) fieldValue;
|
|
|
JSONObject fieldConfig = signField.getJSONObject("normalSignFieldConfig");
|
|
|
String keyword = signField.getString(INTERNAL_SIGN_KEYWORD);
|
|
|
if (!hasText(keyword)) {
|
|
|
+ expandedFields.add(signField);
|
|
|
+ expandedFieldCount++;
|
|
|
continue;
|
|
|
}
|
|
|
- signField.remove(INTERNAL_SIGN_KEYWORD);
|
|
|
String fileId = signField.getString("fileId");
|
|
|
- KeywordPosition position = findFirstKeywordPosition(positionsByFile.get(fileId), keyword);
|
|
|
- if (position == null) {
|
|
|
+ List<KeywordPosition> positions = findKeywordPositions(
|
|
|
+ positionsByFile.get(fileId), keyword);
|
|
|
+ if (positions.isEmpty()) {
|
|
|
throw new IllegalArgumentException("签署关键字未定位到坐标,fileId=" + fileId + ",keyword=" + keyword);
|
|
|
}
|
|
|
if (Boolean.TRUE.equals(fieldConfig.getBoolean("freeMode"))) {
|
|
|
@@ -839,18 +883,28 @@ public class EqbServiceImpl implements EqbService {
|
|
|
if (signFieldStyle != null && signFieldStyle != 1) {
|
|
|
throw new IllegalArgumentException("使用关键字定位时,signFieldStyle必须为1(单页签章)");
|
|
|
}
|
|
|
- fieldConfig.put("signFieldStyle", 1);
|
|
|
- JSONObject signFieldPosition = fieldConfig.getJSONObject("signFieldPosition");
|
|
|
- if (signFieldPosition == null) {
|
|
|
- signFieldPosition = new JSONObject();
|
|
|
- fieldConfig.put("signFieldPosition", signFieldPosition);
|
|
|
+ for (KeywordPosition position : positions) {
|
|
|
+ JSONObject positionedField = JSONObject.parseObject(
|
|
|
+ JSONObject.toJSONString(signField));
|
|
|
+ positionedField.remove(INTERNAL_SIGN_KEYWORD);
|
|
|
+ JSONObject positionedConfig = positionedField.getJSONObject(
|
|
|
+ "normalSignFieldConfig");
|
|
|
+ positionedConfig.put("signFieldStyle", 1);
|
|
|
+ JSONObject signFieldPosition = new JSONObject();
|
|
|
+ signFieldPosition.put("positionPage", String.valueOf(position.pageNum));
|
|
|
+ signFieldPosition.put("positionX", position.positionX);
|
|
|
+ signFieldPosition.put("positionY", position.positionY);
|
|
|
+ positionedConfig.put("signFieldPosition", signFieldPosition);
|
|
|
+ expandedFields.add(positionedField);
|
|
|
+ expandedFieldCount++;
|
|
|
+ LOG.info("e签宝签署区定位成功,fileId={},keyword={},page={},x={},y={}",
|
|
|
+ fileId, keyword, position.pageNum, position.positionX, position.positionY);
|
|
|
}
|
|
|
- signFieldPosition.put("positionPage", String.valueOf(position.pageNum));
|
|
|
- signFieldPosition.put("positionX", position.positionX);
|
|
|
- signFieldPosition.put("positionY", position.positionY);
|
|
|
- LOG.info("e签宝签署区定位成功,fileId={},keyword={},page={},x={},y={}",
|
|
|
- fileId, keyword, position.pageNum, position.positionX, position.positionY);
|
|
|
}
|
|
|
+ signer.put("signFields", expandedFields);
|
|
|
+ }
|
|
|
+ if (expandedFieldCount > 300) {
|
|
|
+ throw new IllegalArgumentException("关键字坐标展开后的签署区总数不能超过300个");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -888,12 +942,17 @@ public class EqbServiceImpl implements EqbService {
|
|
|
* @see <a href="https://open.esign.cn/doc/opendoc/pdf-sign3/ze0ahv">检索文件关键字坐标</a>
|
|
|
*/
|
|
|
protected List<KeywordPosition> queryKeywordPositions(String fileId, List<String> keywords) {
|
|
|
+ return queryKeywordPositions(fileId, keywords, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<KeywordPosition> queryKeywordPositions(
|
|
|
+ String fileId, List<String> keywords, EqbAccountProperties.Account eqbAccount) {
|
|
|
try {
|
|
|
JSONObject request = new JSONObject();
|
|
|
request.put("keywords", new ArrayList<>(new LinkedHashSet<>(keywords)));
|
|
|
String path = "/v3/files/" + URLEncoder.encode(fileId, "UTF-8").replace("+", "%20")
|
|
|
+ "/keyword-positions";
|
|
|
- Map data = requestEqbApi("POST", path, request, 60000);
|
|
|
+ Map data = requestEqbApi("POST", path, request, 60000, eqbAccount);
|
|
|
Object rawKeywordPositions = data.get("keywordPositions");
|
|
|
if (!(rawKeywordPositions instanceof List)) {
|
|
|
throw new IllegalStateException("关键字定位接口未返回keywordPositions数组,fileId=" + fileId);
|
|
|
@@ -942,13 +1001,21 @@ public class EqbServiceImpl implements EqbService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private KeywordPosition findFirstKeywordPosition(List<KeywordPosition> positions, String keyword) {
|
|
|
- for (KeywordPosition position : positions) {
|
|
|
- if (keyword.equals(position.keyword)) {
|
|
|
- return position;
|
|
|
+ private List<KeywordPosition> findKeywordPositions(
|
|
|
+ List<KeywordPosition> positions, String keyword) {
|
|
|
+ List<KeywordPosition> matched = new ArrayList<>();
|
|
|
+ if (positions != null) {
|
|
|
+ for (KeywordPosition position : positions) {
|
|
|
+ if (keyword.equals(position.keyword)) {
|
|
|
+ matched.add(position);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- return null;
|
|
|
+ // 固定页码及页内坐标顺序,确保同一文件每次生成的签署区顺序一致。
|
|
|
+ matched.sort(Comparator.comparingInt((KeywordPosition position) -> position.pageNum)
|
|
|
+ .thenComparingDouble(position -> position.positionY)
|
|
|
+ .thenComparingDouble(position -> position.positionX));
|
|
|
+ return matched;
|
|
|
}
|
|
|
|
|
|
private static class KeywordPosition {
|
|
|
@@ -973,6 +1040,10 @@ public class EqbServiceImpl implements EqbService {
|
|
|
return requestEqbApi("POST", "/v3/files/file-upload-url", request, 60000);
|
|
|
}
|
|
|
|
|
|
+ private Map getFileUploadUrl(JSONObject request, EqbAccountProperties.Account eqbAccount) {
|
|
|
+ return requestEqbApi("POST", "/v3/files/file-upload-url", request, 60000, eqbAccount);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 使用已确认可用的fileId创建并开启签署流程,业务成功时返回signFlowId。
|
|
|
* @see <a href="https://open.esign.cn/doc/opendoc/pdf-sign3/nxhgcl3bfgqz8qlz">基于文件发起签署</a>
|
|
|
@@ -982,8 +1053,26 @@ public class EqbServiceImpl implements EqbService {
|
|
|
return requestEqbApi("POST", "/v3/sign-flow/create-by-file", request, 60000);
|
|
|
}
|
|
|
|
|
|
+ private Map createSignFlow(JSONObject request, EqbAccountProperties.Account eqbAccount) {
|
|
|
+ // 创建流程不自动重试,避免网络响应丢失时重复发起合同。
|
|
|
+ return requestEqbApi("POST", "/v3/sign-flow/create-by-file", request, 60000, eqbAccount);
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
public Map getSignedFileDownloadUrls(String signFlowId) {
|
|
|
+ return getSignedFileDownloadUrls(
|
|
|
+ signFlowId, (EqbAccountProperties.Account) null);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map getSignedFileDownloadUrls(String signFlowId, String ourCompany) {
|
|
|
+ EqbAccountProperties.Account eqbAccount = ourCompany == null
|
|
|
+ ? null : eqbAccountProperties.requireByCompany(ourCompany);
|
|
|
+ return getSignedFileDownloadUrls(signFlowId, eqbAccount);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map getSignedFileDownloadUrls(String signFlowId,
|
|
|
+ EqbAccountProperties.Account eqbAccount) {
|
|
|
if (!hasText(signFlowId)) {
|
|
|
throw new IllegalArgumentException("signFlowId不能为空");
|
|
|
}
|
|
|
@@ -992,7 +1081,7 @@ public class EqbServiceImpl implements EqbService {
|
|
|
try {
|
|
|
String path = "/v3/sign-flow/" + URLEncoder.encode(signFlowId, "UTF-8").replace("+", "%20")
|
|
|
+ "/file-download-url";
|
|
|
- return requestEqbApi("POST", path, request, 60000);
|
|
|
+ return requestEqbApi("POST", path, request, 60000, eqbAccount);
|
|
|
} catch (UnsupportedEncodingException e) {
|
|
|
throw new IllegalStateException("UTF-8编码不可用", e);
|
|
|
}
|
|
|
@@ -1009,11 +1098,19 @@ public class EqbServiceImpl implements EqbService {
|
|
|
return requestEqbApi("GET", path, null, timeoutMillis);
|
|
|
}
|
|
|
|
|
|
+ private Map getFileStatus(String fileId, int timeoutMillis,
|
|
|
+ EqbAccountProperties.Account eqbAccount)
|
|
|
+ throws UnsupportedEncodingException {
|
|
|
+ String path = "/v3/files/" + URLEncoder.encode(fileId, "UTF-8").replace("+", "%20");
|
|
|
+ return requestEqbApi("GET", path, null, timeoutMillis, eqbAccount);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 等待文件达到可签署状态;上传或转换失败立即终止,超时或中断时不发起签署。
|
|
|
* @return 2(上传完成或HTML转换完成)或5(PDF转换完成)
|
|
|
*/
|
|
|
- private int waitForUploadedFile(String fileId) throws Exception {
|
|
|
+ private int waitForUploadedFile(String fileId, EqbAccountProperties.Account eqbAccount)
|
|
|
+ throws Exception {
|
|
|
// 使用单调时钟计算实际耗时,避免系统时间调整影响90秒截止时间。
|
|
|
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(FILE_STATUS_POLL_TIMEOUT_MILLIS);
|
|
|
int maxAttempts = (int) (FILE_STATUS_POLL_TIMEOUT_MILLIS / FILE_STATUS_POLL_INTERVAL_MILLIS);
|
|
|
@@ -1029,7 +1126,7 @@ public class EqbServiceImpl implements EqbService {
|
|
|
// 单次查询最多15秒,临近截止时间时缩短请求预算,查询耗时也计入轮询时长。
|
|
|
Map data;
|
|
|
try {
|
|
|
- data = getFileStatus(fileId, (int) Math.min(15000, remaining));
|
|
|
+ data = getFileStatus(fileId, (int) Math.min(15000, remaining), eqbAccount);
|
|
|
} catch (IllegalStateException e) {
|
|
|
if (System.nanoTime() >= deadline) {
|
|
|
break;
|
|
|
@@ -1103,6 +1200,9 @@ public class EqbServiceImpl implements EqbService {
|
|
|
JSONObject noticeConfig = new JSONObject();
|
|
|
noticeConfig.put("noticeTypes","1");
|
|
|
config.put("noticeConfig",noticeConfig);
|
|
|
+ if (hasText(eqbCallbackUrl)) {
|
|
|
+ config.put("notifyUrl", eqbCallbackUrl.trim());
|
|
|
+ }
|
|
|
JSONArray signers = JSONArray.parseArray(JSONObject.toJSONString(rawSigners));
|
|
|
int fieldCount = 0;
|
|
|
for (Object value : signers) {
|
|
|
@@ -1146,12 +1246,9 @@ public class EqbServiceImpl implements EqbService {
|
|
|
}
|
|
|
if (type == 0) {
|
|
|
validateSignerAccount(signer.getJSONObject("psnSignerInfo"));
|
|
|
- } else if (!allAutoSign) {
|
|
|
- JSONObject org = signer.getJSONObject("orgSignerInfo");
|
|
|
- if (org == null || (!hasText(org.getString("orgId")) && !hasText(org.getString("orgName")))) {
|
|
|
- throw new IllegalArgumentException("企业签署方必须提供orgSignerInfo.orgName或orgId");
|
|
|
- }
|
|
|
- validateSignerAccount(org.getJSONObject("transactorInfo"));
|
|
|
+ } else if (type == 1 && allAutoSign) {
|
|
|
+ // 机构自动签署由当前appId确定签署主体,不能再传orgSignerInfo。
|
|
|
+ signer.remove("orgSignerInfo");
|
|
|
}
|
|
|
}
|
|
|
if ((long) fieldCount * fileCount > 300) {
|
|
|
@@ -1191,26 +1288,59 @@ public class EqbServiceImpl implements EqbService {
|
|
|
* @return e签宝响应中的data对象,不包含外层code、message
|
|
|
*/
|
|
|
protected Map requestEqbApi(String method, String path, JSONObject body, int timeoutMillis) {
|
|
|
+ return executeEqbApi(method, path, path, body, timeoutMillis, appId, appKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected Map requestEqbApi(String method, String path, JSONObject body, int timeoutMillis,
|
|
|
+ EqbAccountProperties.Account eqbAccount) {
|
|
|
+ // null仅供历史入口和单元测试使用;宜搭自动落印始终显式传入公司账号。
|
|
|
+ if (eqbAccount == null) {
|
|
|
+ return requestEqbApi(method, path, body, timeoutMillis);
|
|
|
+ }
|
|
|
+ return executeEqbApi(method, path, path, body, timeoutMillis,
|
|
|
+ eqbAccount.getAppId(), eqbAccount.getAppKey());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 中文Query参数使用原值签名、编码值发送时调用,其余请求的两个路径保持一致。 */
|
|
|
+ protected Map requestEqbApi(String method, String requestPath, String signaturePath,
|
|
|
+ JSONObject body, int timeoutMillis,
|
|
|
+ EqbAccountProperties.Account eqbAccount) {
|
|
|
+ if (eqbAccount == null) {
|
|
|
+ return executeEqbApi(method, requestPath, signaturePath, body, timeoutMillis,
|
|
|
+ appId, appKey);
|
|
|
+ }
|
|
|
+ return executeEqbApi(method, requestPath, signaturePath, body, timeoutMillis,
|
|
|
+ eqbAccount.getAppId(), eqbAccount.getAppKey());
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map executeEqbApi(String method, String requestPath, String signaturePath,
|
|
|
+ JSONObject body, int timeoutMillis,
|
|
|
+ String requestAppId, String requestAppKey) {
|
|
|
HttpURLConnection connection = null;
|
|
|
try {
|
|
|
+ if (!hasText(requestAppId) || !hasText(requestAppKey)) {
|
|
|
+ throw new IllegalStateException("e签宝appId或appKey未配置");
|
|
|
+ }
|
|
|
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
|
|
|
String bodyText = body == null ? "" : body.toJSONString();
|
|
|
// API鉴权的MD5针对JSON请求体,与PUT文件流使用的文件MD5含义不同。
|
|
|
String contentMd5 = body == null ? "" : doContentMD5(bodyText);
|
|
|
String contentType = "application/json; charset=UTF-8";
|
|
|
// 按协议顺序拼接:方法、Accept、Content-MD5、Content-Type、空Date、请求路径。
|
|
|
- String plaintext = method + "\n*/*\n" + contentMd5 + "\n" + contentType + "\n\n" + path;
|
|
|
- connection = (HttpURLConnection) new URL(eqbApiHost + path).openConnection();
|
|
|
+ String plaintext = method + "\n*/*\n" + contentMd5 + "\n" + contentType
|
|
|
+ + "\n\n" + signaturePath;
|
|
|
+ connection = (HttpURLConnection) new URL(eqbApiHost + requestPath).openConnection();
|
|
|
connection.setRequestMethod(method);
|
|
|
connection.setConnectTimeout(Math.min(15000, timeoutMillis));
|
|
|
connection.setReadTimeout(timeoutMillis);
|
|
|
connection.setInstanceFollowRedirects(false);
|
|
|
connection.setRequestProperty("Accept", "*/*");
|
|
|
connection.setRequestProperty("Content-Type", contentType);
|
|
|
- connection.setRequestProperty("X-Tsign-Open-App-Id", appId);
|
|
|
+ connection.setRequestProperty("X-Tsign-Open-App-Id", requestAppId);
|
|
|
connection.setRequestProperty("X-Tsign-Open-Auth-Mode", "Signature");
|
|
|
connection.setRequestProperty("X-Tsign-Open-Ca-Timestamp", String.valueOf(timeStamp()));
|
|
|
- connection.setRequestProperty("X-Tsign-Open-Ca-Signature", doSignatureBase64(plaintext, appKey));
|
|
|
+ connection.setRequestProperty("X-Tsign-Open-Ca-Signature",
|
|
|
+ doSignatureBase64(plaintext, requestAppKey));
|
|
|
if (body != null) {
|
|
|
byte[] bytes = bodyText.getBytes(StandardCharsets.UTF_8);
|
|
|
connection.setRequestProperty("Content-MD5", contentMd5);
|
|
|
@@ -1227,23 +1357,13 @@ public class EqbServiceImpl implements EqbService {
|
|
|
String responseMessage = connection.getResponseMessage();
|
|
|
int status = connection.getResponseCode();
|
|
|
if (status < 200 || status >= 300) {
|
|
|
- throw new IOException("HTTP状态码:" + status);
|
|
|
- }
|
|
|
- StringBuilder responseText = new StringBuilder();
|
|
|
- try (BufferedReader reader = new BufferedReader(
|
|
|
- new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
|
|
- String line;
|
|
|
- while (true) {
|
|
|
- connection.setReadTimeout(remainingRequestMillis(deadline));
|
|
|
- line = reader.readLine();
|
|
|
- if (line == null) {
|
|
|
- break;
|
|
|
- }
|
|
|
- responseText.append(line);
|
|
|
- }
|
|
|
+ String errorBody = readEqbResponse(connection.getErrorStream());
|
|
|
+ throw new IOException("HTTP状态码:" + status
|
|
|
+ + (hasText(errorBody) ? ",响应:" + errorBody : ""));
|
|
|
}
|
|
|
+ String responseText = readEqbResponse(connection.getInputStream());
|
|
|
// 普通API成功码是code=0;文件流PUT接口使用errCode=0,二者不能混用。
|
|
|
- JSONObject response = JSONObject.parseObject(responseText.toString());
|
|
|
+ JSONObject response = JSONObject.parseObject(responseText);
|
|
|
if (response == null || !Integer.valueOf(0).equals(response.getInteger("code"))) {
|
|
|
throw new IllegalStateException(response == null ? "返回空响应"
|
|
|
: "code=" + response.getString("code") + ",message=" + response.getString("message"));
|
|
|
@@ -1253,7 +1373,8 @@ public class EqbServiceImpl implements EqbService {
|
|
|
}
|
|
|
return response.getJSONObject("data");
|
|
|
} catch (Exception e) {
|
|
|
- throw new IllegalStateException("e签宝接口" + path + "调用失败:" + e.getMessage(), e);
|
|
|
+ throw new IllegalStateException(
|
|
|
+ "e签宝接口" + requestPath + "调用失败:" + e.getMessage(), e);
|
|
|
} finally {
|
|
|
if (connection != null) {
|
|
|
connection.disconnect();
|
|
|
@@ -1261,6 +1382,26 @@ public class EqbServiceImpl implements EqbService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 读取成功或失败响应体;失败响应保留业务错误码,便于区分签名错误与接口权限不足。 */
|
|
|
+ private String readEqbResponse(InputStream input) throws IOException {
|
|
|
+ if (input == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ StringBuilder responseText = new StringBuilder();
|
|
|
+ try (BufferedReader reader = new BufferedReader(
|
|
|
+ new InputStreamReader(input, StandardCharsets.UTF_8))) {
|
|
|
+ String line;
|
|
|
+ while (true) {
|
|
|
+ line = reader.readLine();
|
|
|
+ if (line == null) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ responseText.append(line);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return responseText.toString();
|
|
|
+ }
|
|
|
+
|
|
|
/** 计算请求剩余时间;耗尽时抛出异常,避免向连接传入0而变成无限等待。 */
|
|
|
private int remainingRequestMillis(long deadline) throws SocketTimeoutException {
|
|
|
long remaining = TimeUnit.NANOSECONDS.toMillis(deadline - System.nanoTime());
|
|
|
@@ -1481,7 +1622,9 @@ public class EqbServiceImpl implements EqbService {
|
|
|
noticeConfig.put("noticeTypes","2");
|
|
|
signFlowConfig.put("noticeConfig",noticeConfig);
|
|
|
signFlowConfig.put("autoFinish",true);
|
|
|
- signFlowConfig.put("notifyUrl",serverHost + "/huagao/eqb/callback");//回调地址
|
|
|
+ if (hasText(eqbCallbackUrl)) {
|
|
|
+ signFlowConfig.put("notifyUrl", eqbCallbackUrl.trim());//签署流程完结回调地址
|
|
|
+ }
|
|
|
reqBodyObj2.put("signFlowConfig",signFlowConfig);
|
|
|
|
|
|
//签署方信息
|