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; +/