From 38b6f9c89db012bafa1756ee366fcce19996bd14 Mon Sep 17 00:00:00 2001
From: zhouxiaofeng <2946471396@qq.com>
Date: Wed, 3 Jun 2026 16:46:10 +0800
Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../contractlaunch/dto/ContractLaunchDto.java | 3 +
.../dto/ContractLaunchSealDto.java | 3 +
.../service/ContractLaunchServiceImpl.java | 69 +++++-
.../nbport/zgwl/service/LocalFileService.java | 128 +++++++++++
.../contractlaunch/ContractLaunchMapper.xml | 8 +-
.../zgwl/test/SealDownloadManualTest.java | 207 ++++++++++++++++++
zgwl_contract_launch_signed_url_patch.sql | 16 ++
7 files changed, 430 insertions(+), 4 deletions(-)
create mode 100644 src/test/java/com/nbport/zgwl/test/SealDownloadManualTest.java
create mode 100644 zgwl_contract_launch_signed_url_patch.sql
diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchDto.java
index 004c6e9..ddea503 100644
--- a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchDto.java
+++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchDto.java
@@ -275,6 +275,9 @@ public class ContractLaunchDto implements Serializable {
/** 签署完成后的合同文件大小(字节) */
private Long signedFileSize;
+ /** 在线签章平台原始下载URL(保留原始地址,signedFilePath 存本地路径) */
+ private String signedFileUrl;
+
/** 未在线签署的原因(线下签署时填写) */
private String signedOfflineReason;
diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchSealDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchSealDto.java
index b978f8f..874c799 100644
--- a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchSealDto.java
+++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchSealDto.java
@@ -36,5 +36,8 @@ public class ContractLaunchSealDto implements Serializable {
private Long signedFileSize;
/** 未在线签署的原因(线下签署时填写) */
+ /** 在线签章平台原始下载URL(云平台签署完成时返回的下载地址) */
+ private String signedFileUrl;
+
private String signedOfflineReason;
}
diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchServiceImpl.java b/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchServiceImpl.java
index 43bc662..fd85427 100644
--- a/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchServiceImpl.java
+++ b/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchServiceImpl.java
@@ -21,6 +21,12 @@ import com.nbport.zgwl.contractlaunch.utils.JsonUtils;
import com.nbport.zgwl.contractpush.dto.PushPayloadDTO;
import com.nbport.zgwl.contractpush.service.DataCollectionPushService;
import com.nbport.zgwl.contractpush.service.SettlementPushService;
+import com.nbport.zgwl.seal.service.SealTokenService;
+import com.nbport.zgwl.seal.config.SealConfig;
+import com.nbport.zgwl.seal.utils.SealHttpClient;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.nbport.zgwl.service.LocalFileService;
import com.nbport.zgwl.entity.SysUserEntity;
import com.nbport.zgwl.exception.ServiceException;
import com.nbport.zgwl.utils.AdminSecurityManage;
@@ -54,6 +60,9 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
private final SettlementPushService settlementPushService;
private final DataCollectionPushService dataCollectionPushService;
private final ContractLaunchLegalFileBridgeService legalFileBridgeService;
+ private final LocalFileService localFileService;
+ private final SealTokenService sealTokenService;
+ private final SealConfig sealConfig;
/** 分页查询,附带合同方/文件/履行计划(列表页不查日志) */
@Override
@@ -390,7 +399,10 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
contract.setSealContractId(sealDto.getSealContractId() == null || sealDto.getSealContractId().isBlank() ? contract.getSealContractId() : sealDto.getSealContractId());
contract.setSealContractStatus(sealDto.getSealContractStatus());
contract.setSignedFileName(sealDto.getSignedFileName());
- contract.setSignedFilePath(sealDto.getSignedFilePath());
+ contract.setSignedFilePath(resolveSignedFilePath(sealDto));
+ contract.setSignedFileUrl(sealDto.getSignedFileUrl() != null && !sealDto.getSignedFileUrl().isBlank()
+ ? sealDto.getSignedFileUrl()
+ : (isExternalUrl(sealDto.getSignedFilePath()) ? sealDto.getSignedFilePath() : null));
contract.setSignedFileType(sealDto.getSignedFileType());
contract.setSignedFileSize(sealDto.getSignedFileSize());
contract.setSignedOfflineReason(sealDto.getSignedOfflineReason());
@@ -1261,6 +1273,61 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
.collect(Collectors.toList());
}
+ /**
+ * 如果 signedFilePath 是外部URL,则下载到本地并返回本地路径;
+ * 如果已是本地路径则直接返回。
+ */
+ private String resolveSignedFilePath(ContractLaunchSealDto sealDto) {
+ String filePath = sealDto.getSignedFilePath();
+ if (filePath == null || filePath.isBlank()) {
+ return filePath;
+ }
+ if (isExternalUrl(filePath)) {
+ log.info("签章文件为外部URL,先通过签章API获取新下载地址: {}", filePath);
+ String token = sealTokenService.getAccessToken();
+ // 通过签章API获取服务端可用的下载URL
+ String freshUrl = fetchSealDownloadUrl(sealDto.getSealContractId(), token, filePath);
+ log.info("获取到签章下载地址: {}", freshUrl);
+ String localPath = localFileService.downloadFromUrl(freshUrl, token);
+ log.info("签章文件已下载到本地: {}", localPath);
+ return localPath;
+ }
+ return filePath;
+ }
+
+ /**
+ * 通过签章平台API获取下载URL(服务端IP绑定的新URL)
+ */
+ private String fetchSealDownloadUrl(String sealContractId, String token, String fallbackUrl) {
+ try {
+ String apiUrl = sealConfig.getBaseUrl() + "/saas/api/contract/download";
+ JSONObject body = new JSONObject();
+ body.put("contractId", sealContractId);
+ String response = SealHttpClient.post(apiUrl, body.toJSONString(), token, "application/json");
+ log.info("签章平台下载API响应: {}", response);
+ JSONObject result = JSON.parseObject(response);
+ if (result != null && result.getBooleanValue("isSuccess")) {
+ Object dataObj = result.get("data");
+ log.info("签章平台data类型: {}, 值: {}", dataObj != null ? dataObj.getClass().getSimpleName() : "null", dataObj);
+ if (dataObj instanceof JSONObject) {
+ String url = ((JSONObject) dataObj).getString("downloadUrl");
+ if (url != null && !url.isBlank()) return url;
+ } else if (dataObj instanceof String && !((String) dataObj).isBlank()) {
+ return (String) dataObj;
+ }
+ } else {
+ log.warn("签章平台下载API返回失败: {}", response);
+ }
+ } catch (Exception e) {
+ log.warn("通过签章API获取下载URL失败,使用原始URL: {}", e.getMessage());
+ }
+ return fallbackUrl;
+ }
+
+ private boolean isExternalUrl(String path) {
+ return path != null && (path.startsWith("http://") || path.startsWith("https://"));
+ }
+
private String trim(String value) {
return value == null ? null : value.trim();
}
diff --git a/src/main/java/com/nbport/zgwl/service/LocalFileService.java b/src/main/java/com/nbport/zgwl/service/LocalFileService.java
index 30c74e1..27a1318 100644
--- a/src/main/java/com/nbport/zgwl/service/LocalFileService.java
+++ b/src/main/java/com/nbport/zgwl/service/LocalFileService.java
@@ -17,6 +17,11 @@ import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -79,6 +84,128 @@ public class LocalFileService {
}
}
+ /**
+ * 从外部URL下载文件并保存到本地,返回本地相对路径(如 20260603/uuid.pdf)。
+ * 文件名优先从 Content-Disposition 或 URL 路径提取,否则使用兜底名。
+ */
+ public String downloadFromUrl(String fileUrl) {
+ return downloadFromUrl(fileUrl, null);
+ }
+
+ public String downloadFromUrl(String fileUrl, String accessToken) {
+ if (fileUrl == null || fileUrl.isBlank()) {
+ throw new ServiceException("下载URL不能为空");
+ }
+ // 先尝试不带 token(签章平台下载 URL 自带 JWT 认证)
+ try {
+ return doDownload(fileUrl, null);
+ } catch (ServiceException e) {
+ // 如果 401/403,再尝试带 token
+ if (accessToken != null && !accessToken.isBlank() && e.getMessage().contains("40")) {
+ log.info("无token下载失败,尝试带token重试: {}", fileUrl);
+ return doDownload(fileUrl, accessToken);
+ }
+ throw e;
+ }
+ }
+
+ private String doDownload(String fileUrl, String accessToken) {
+ HttpURLConnection conn = null;
+ try {
+ String cleanUrl = fileUrl.contains("?") ? fileUrl.substring(0, fileUrl.indexOf("?")) : fileUrl;
+ URL url = new URL(cleanUrl);
+ conn = openConnection(url);
+ conn.setConnectTimeout(30000);
+ conn.setReadTimeout(60000);
+ conn.setRequestProperty("User-Agent", "Mozilla/5.0");
+ if (accessToken != null && !accessToken.isBlank()) {
+ conn.setRequestProperty("access_token", accessToken);
+ }
+
+ int responseCode = conn.getResponseCode();
+ if (responseCode < 200 || responseCode >= 300) {
+ // 读取错误响应体用于排查
+ String errorBody = "";
+ try (InputStream es = conn.getErrorStream()) {
+ if (es != null) {
+ errorBody = new String(es.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ } catch (Exception ignored) {}
+ log.error("下载签章文件失败, URL={}, code={}, body={}", fileUrl, responseCode, errorBody);
+ throw new ServiceException("下载签章文件失败,HTTP状态码: " + responseCode + ", 详情: " + errorBody);
+ }
+
+ String fileName = extractFileName(fileUrl, conn);
+ String suffix = FileStorageUtils.getFileSuffix(fileName);
+
+ Path rootPath = Paths.get(localPath).toAbsolutePath().normalize();
+ Files.createDirectories(rootPath);
+ String dateFolder = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
+ String storedName = IdUtil.simpleUUID() + suffix;
+ String relativePath = dateFolder + "/" + storedName;
+ Path targetPath = FileStorageUtils.resolveFilePath(rootPath, relativePath);
+ Files.createDirectories(targetPath.getParent());
+
+ try (InputStream is = conn.getInputStream()) {
+ Files.copy(is, targetPath);
+ }
+
+ log.info("从URL下载文件成功: {} -> {}", fileUrl, targetPath);
+ return relativePath;
+ } catch (IOException e) {
+ log.error("从URL下载文件失败: {}", fileUrl, e);
+ throw new ServiceException("下载签章文件失败: " + e.getMessage());
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+
+ private HttpURLConnection openConnection(URL url) throws IOException {
+ try {
+ if ("https".equalsIgnoreCase(url.getProtocol())) {
+ javax.net.ssl.HttpsURLConnection httpsConn = (javax.net.ssl.HttpsURLConnection) url.openConnection();
+ javax.net.ssl.SSLContext sslContext = javax.net.ssl.SSLContext.getInstance("TLS");
+ sslContext.init(null, new javax.net.ssl.TrustManager[]{
+ new javax.net.ssl.X509TrustManager() {
+ public void checkClientTrusted(java.security.cert.X509Certificate[] c, String a) {}
+ public void checkServerTrusted(java.security.cert.X509Certificate[] c, String a) {}
+ public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new java.security.cert.X509Certificate[0]; }
+ }
+ }, new java.security.SecureRandom());
+ httpsConn.setSSLSocketFactory(sslContext.getSocketFactory());
+ httpsConn.setHostnameVerifier((hostname, session) -> true);
+ return httpsConn;
+ }
+ return (HttpURLConnection) url.openConnection();
+ } catch (Exception e) {
+ log.warn("SSL初始化失败,使用默认连接: {}", e.getMessage());
+ return (HttpURLConnection) url.openConnection();
+ }
+ }
+
+ private String extractFileName(String urlStr, HttpURLConnection conn) {
+ String disposition = conn.getHeaderField("Content-Disposition");
+ if (disposition != null) {
+ for (String part : disposition.split(";")) {
+ part = part.trim();
+ if (part.toLowerCase().startsWith("filename=")) {
+ String name = part.substring(part.indexOf('=') + 1).replaceAll("^[\"\']|[\"\']$", "").trim();
+ if (!name.isBlank()) return name;
+ }
+ }
+ }
+ String path = urlStr.contains("?") ? urlStr.substring(0, urlStr.indexOf('?')) : urlStr;
+ String name = path.substring(path.lastIndexOf('/') + 1);
+ String suffix = FileStorageUtils.getFileSuffix(name);
+ // 后缀过长(>10)说明是JWT/乱码而非真实文件名, 默认pdf
+ if (!name.isBlank() && suffix.length() > 0 && suffix.length() <= 10) {
+ return URLDecoder.decode(name, StandardCharsets.UTF_8);
+ }
+ return "signed_contract.pdf";
+ }
+
public byte[] readFileBytes(String fileName) {
try {
Path rootPath = Paths.get(localPath).toAbsolutePath().normalize();
@@ -92,4 +219,5 @@ public class LocalFileService {
throw new ServiceException("文件读取失败");
}
}
+
}
diff --git a/src/main/resources/mapper/contractlaunch/ContractLaunchMapper.xml b/src/main/resources/mapper/contractlaunch/ContractLaunchMapper.xml
index b74a4c1..24964e0 100644
--- a/src/main/resources/mapper/contractlaunch/ContractLaunchMapper.xml
+++ b/src/main/resources/mapper/contractlaunch/ContractLaunchMapper.xml
@@ -75,6 +75,7 @@
+
@@ -181,7 +182,7 @@
BIDDING_NO, OPEN_BIDDING_REMARK, CONTRACT_GIST_REMARK,
EW_AMOUNT_TYPE, DJ_AMOUNT, YJ_AMOUNT, YFK_AMOUNT, BZJ_AMOUNT, BODY_SUMMARY,
CONTRACT_TEXT_FILE_NAME, CONTRACT_TEXT_FILE_PATH, CONTRACT_TEXT_FILE_TYPE, CONTRACT_TEXT_FILE_SIZE,
- SIGNED_FILE_NAME, SIGNED_FILE_PATH, SIGNED_FILE_TYPE, SIGNED_FILE_SIZE, SIGNED_OFFLINE_REASON,
+ SIGNED_FILE_NAME, SIGNED_FILE_PATH, SIGNED_FILE_TYPE, SIGNED_FILE_SIZE, SIGNED_FILE_URL, SIGNED_OFFLINE_REASON,
SIGN_DATE, ACTIVE_DATE, ARCHIVE_DATE, ARCHIVE_KEEPER, ARCHIVE_KEEPER_UNIT,
ARCHIVE_KEEPER_UNIT_CODE, INVOICE_DATE, RECEIVE_DATE, REMIND_DAYS,
INITIATOR, INITIATOR_DEPT, COMPANY_NAME, ORG_ID, QUERY_SCOPE,
@@ -259,7 +260,7 @@
EW_AMOUNT_TYPE, DJ_AMOUNT, YJ_AMOUNT, YFK_AMOUNT, BZJ_AMOUNT, BODY_SUMMARY,
TEMPLATE_FIELD_VALUES_JSON,
CONTRACT_TEXT_FILE_NAME, CONTRACT_TEXT_FILE_PATH, CONTRACT_TEXT_FILE_TYPE, CONTRACT_TEXT_FILE_SIZE,
- SIGNED_FILE_NAME, SIGNED_FILE_PATH, SIGNED_FILE_TYPE, SIGNED_FILE_SIZE, SIGNED_OFFLINE_REASON,
+ SIGNED_FILE_NAME, SIGNED_FILE_PATH, SIGNED_FILE_TYPE, SIGNED_FILE_SIZE, SIGNED_FILE_URL, SIGNED_OFFLINE_REASON,
SIGN_DATE, ACTIVE_DATE, ARCHIVE_DATE, ARCHIVE_KEEPER, ARCHIVE_KEEPER_UNIT,
ARCHIVE_KEEPER_UNIT_CODE, INVOICE_DATE, RECEIVE_DATE, REMIND_DAYS,
INITIATOR, INITIATOR_DEPT, COMPANY_NAME, ORG_ID, QUERY_SCOPE,
@@ -279,7 +280,7 @@
#{ewAmountType}, #{djAmount}, #{yjAmount}, #{yfkAmount}, #{bzjAmount}, #{bodySummary},
#{templateFieldValuesJson},
#{contractTextFileName}, #{contractTextFilePath}, #{contractTextFileType}, #{contractTextFileSize},
- #{signedFileName}, #{signedFilePath}, #{signedFileType}, #{signedFileSize}, #{signedOfflineReason},
+ #{signedFileName}, #{signedFilePath}, #{signedFileType}, #{signedFileSize}, #{signedFileUrl}, #{signedOfflineReason},
#{signDate}, #{activeDate}, #{archiveDate}, #{archiveKeeper}, #{archiveKeeperUnit},
#{archiveKeeperUnitCode}, #{invoiceDate}, #{receiveDate}, #{remindDays},
#{initiator}, #{initiatorDept}, #{companyName}, #{orgId}, #{queryScope},
@@ -359,6 +360,7 @@
SIGNED_FILE_PATH = #{signedFilePath},
SIGNED_FILE_TYPE = #{signedFileType},
SIGNED_FILE_SIZE = #{signedFileSize},
+ SIGNED_FILE_URL = #{signedFileUrl},
SIGNED_OFFLINE_REASON = #{signedOfflineReason},
SIGN_DATE = #{signDate},
ACTIVE_DATE = #{activeDate},
diff --git a/src/test/java/com/nbport/zgwl/test/SealDownloadManualTest.java b/src/test/java/com/nbport/zgwl/test/SealDownloadManualTest.java
new file mode 100644
index 0000000..9c8f6a3
--- /dev/null
+++ b/src/test/java/com/nbport/zgwl/test/SealDownloadManualTest.java
@@ -0,0 +1,207 @@
+package com.nbport.zgwl.test;
+
+import com.nbport.zgwl.seal.utils.SealHttpClient;
+import com.nbport.zgwl.seal.utils.HMacSHA256Util;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import org.springframework.http.MediaType;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.cert.X509Certificate;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+
+public class SealDownloadManualTest {
+
+ private static final String BASE_URL = "https://saasapi.certca.cn";
+ private static final String APP_KEY = "A7C4FD32BE454B5";
+ private static final String APP_SECRET = "6c74358c28c149f18242bd614a101910";
+ private static final String SAVE_DIR = "D:/temp-seal-download";
+
+ public static void main(String[] args) throws Exception {
+ String contractId = args.length > 0 ? args[0] : "63b5552bfebe44a9de0d16d39162b678";
+ System.out.println("===== Seal Download Test =====");
+
+ String token = getAccessToken();
+ System.out.println("[1] token OK");
+
+ JSONObject body = new JSONObject();
+ body.put("contractId", contractId);
+ String response = SealHttpClient.post(
+ BASE_URL + "/saas/api/contract/download",
+ body.toJSONString(), token, MediaType.APPLICATION_JSON_VALUE);
+ JSONObject result = JSON.parseObject(response);
+ if (result == null || !result.getBooleanValue("isSuccess")) {
+ System.err.println("[FAIL] " + response);
+ return;
+ }
+ Object dataObj = result.get("data");
+ String rawUrl = null;
+ if (dataObj instanceof JSONObject) {
+ rawUrl = ((JSONObject) dataObj).getString("downloadUrl");
+ } else if (dataObj instanceof String) {
+ rawUrl = (String) dataObj;
+ }
+ System.out.println("[2] rawUrl: " + rawUrl);
+
+ // 尝试多种方式下载
+ System.out.println("\n[3] Trying download methods...");
+
+ // 方法1: 直接GET
+ System.out.println(" [3a] Direct GET...");
+ tryDownload(rawUrl, null, false);
+
+ // 方法2: 带 access_token header
+ System.out.println(" [3b] GET + access_token header...");
+ tryDownload(rawUrl, token, false);
+
+ // 方法3: URL编码中文参数后再GET
+ String encodedUrl = encodeChineseInUrl(rawUrl);
+ if (!encodedUrl.equals(rawUrl)) {
+ System.out.println(" [3c] GET with URL-encoded params...");
+ tryDownload(encodedUrl, null, false);
+ System.out.println(" [3d] GET URL-encoded + access_token...");
+ tryDownload(encodedUrl, token, false);
+ }
+
+ // 方法4: POST方式
+ System.out.println(" [3e] POST download (as API)...");
+ tryPostDownload(rawUrl, token);
+
+ // 方法5: 去掉query string
+ String noQuery = rawUrl.contains("?") ? rawUrl.substring(0, rawUrl.indexOf('?')) : rawUrl;
+ System.out.println(" [3f] GET without query string...");
+ tryDownload(noQuery, token, false);
+
+ System.out.println("\n===== Done =====");
+ }
+
+ private static String encodeChineseInUrl(String urlStr) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < urlStr.length(); i++) {
+ char c = urlStr.charAt(i);
+ if (c > 127) {
+ sb.append(URLEncoder.encode(String.valueOf(c), StandardCharsets.UTF_8));
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static void tryDownload(String fileUrl, String token, boolean isPost) {
+ try {
+ URL url = new URL(fileUrl);
+ HttpURLConnection conn = openConnection(url);
+ conn.setConnectTimeout(15000);
+ conn.setReadTimeout(30000);
+ conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
+ if (token != null && !token.isBlank()) {
+ conn.setRequestProperty("access_token", token);
+ }
+ if (isPost) {
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setRequestProperty("Content-Type", "application/json");
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write("{}".getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ int code = conn.getResponseCode();
+ if (code >= 200 && code < 300) {
+ long size = conn.getContentLengthLong();
+ System.out.println(" [OK] HTTP " + code + ", size=" + size);
+ } else {
+ String err = readStream(conn.getErrorStream());
+ System.out.println(" [FAIL] HTTP " + code + " body=" + err);
+ }
+ conn.disconnect();
+ } catch (Exception e) {
+ System.out.println(" [ERR] " + e.getMessage());
+ }
+ }
+
+ private static void tryPostDownload(String fileUrl, String token) {
+ try {
+ URL url = new URL(fileUrl);
+ HttpURLConnection conn = openConnection(url);
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(15000);
+ conn.setReadTimeout(30000);
+ conn.setRequestProperty("User-Agent", "Mozilla/5.0");
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ if (token != null && !token.isBlank()) {
+ conn.setRequestProperty("access_token", token);
+ }
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(new byte[0]);
+ }
+ int code = conn.getResponseCode();
+ if (code >= 200 && code < 300) {
+ System.out.println(" [OK] HTTP " + code);
+ } else {
+ String err = readStream(conn.getErrorStream());
+ System.out.println(" [FAIL] HTTP " + code + " body=" + err);
+ }
+ conn.disconnect();
+ } catch (Exception e) {
+ System.out.println(" [ERR] " + e.getMessage());
+ }
+ }
+
+ private static String getAccessToken() throws Exception {
+ long ts = System.currentTimeMillis();
+ JSONObject signData = new JSONObject();
+ signData.put("appKey", APP_KEY);
+ signData.put("timestamp", ts);
+ String sig = HMacSHA256Util.encrypt(JSON.toJSONString(signData), APP_SECRET);
+ JSONObject req = new JSONObject();
+ req.put("appKey", APP_KEY);
+ req.put("timestamp", ts);
+ req.put("signature", sig);
+ String resp = SealHttpClient.post(
+ BASE_URL + "/saas/api/token/getAccessToken",
+ req.toJSONString(), null, MediaType.APPLICATION_JSON_VALUE);
+ return JSON.parseObject(resp).getJSONObject("data").getString("access_token");
+ }
+
+ private static HttpURLConnection openConnection(URL url) throws Exception {
+ if ("https".equalsIgnoreCase(url.getProtocol())) {
+ HttpsURLConnection h = (HttpsURLConnection) url.openConnection();
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(null, new TrustManager[]{new X509TrustManager() {
+ public void checkClientTrusted(X509Certificate[] c, String a) {}
+ public void checkServerTrusted(X509Certificate[] c, String a) {}
+ public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
+ }}, new java.security.SecureRandom());
+ h.setSSLSocketFactory(ctx.getSocketFactory());
+ h.setHostnameVerifier((host, sess) -> true);
+ return h;
+ }
+ return (HttpURLConnection) url.openConnection();
+ }
+
+ private static String readStream(InputStream is) throws IOException {
+ if (is == null) return "";
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String line; while ((line = r.readLine()) != null) sb.append(line);
+ }
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/zgwl_contract_launch_signed_url_patch.sql b/zgwl_contract_launch_signed_url_patch.sql
new file mode 100644
index 0000000..78d0177
--- /dev/null
+++ b/zgwl_contract_launch_signed_url_patch.sql
@@ -0,0 +1,16 @@
+-- 补充 SIGNED_FILE_URL 字段(在线签章原始下载地址)
+-- 幂等执行:如已存在则跳过
+DECLARE
+ v_count NUMBER;
+BEGIN
+ SELECT COUNT(*) INTO v_count FROM ALL_TAB_COLUMNS
+ WHERE OWNER = 'DEVELOPER' AND TABLE_NAME = 'SEAL_CONTRACT_LAUNCH_MAIN' AND COLUMN_NAME = 'SIGNED_FILE_URL';
+ IF v_count = 0 THEN
+ EXECUTE IMMEDIATE 'ALTER TABLE SEAL_CONTRACT_LAUNCH_MAIN ADD SIGNED_FILE_URL VARCHAR2(500)';
+ EXECUTE IMMEDIATE 'COMMENT ON COLUMN SEAL_CONTRACT_LAUNCH_MAIN.SIGNED_FILE_URL IS ''在线签章平台原始下载地址''';
+ DBMS_OUTPUT.PUT_LINE('SIGNED_FILE_URL column added.');
+ ELSE
+ DBMS_OUTPUT.PUT_LINE('SIGNED_FILE_URL already exists, skipped.');
+ END IF;
+END;
+/