package com.project.receive.strategy.handler; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.project.logistics.config.FixedRuleProperties; import com.project.logistics.config.SfApiProperties; import com.project.logistics.config.WebDavProperties; import com.project.logistics.domain.entity.ApiRetryTaskEntity; import com.project.logistics.domain.entity.LogisticsOrderEntity; import com.project.logistics.domain.enums.OrderStatusEnum; import com.project.logistics.domain.enums.RetryActionEnum; import com.project.logistics.domain.service.WebDavService; import com.project.logistics.domain.service.base.LogisticsOrderService; import com.project.logistics.domain.strategy.ApiTaskHandler; import com.project.logistics.domain.utils.FilePathUtil; import com.project.receive.domain.entity.PodPushLogEntity; import com.project.receive.domain.service.base.PodPushLogService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.spec.AlgorithmParameterSpec; import java.util.Base64; import java.util.Date; @Slf4j @Component public class SfDownloadPodHandler implements ApiTaskHandler { @Autowired private PodPushLogService podPushLogService; @Autowired private LogisticsOrderService logisticsOrderService; @Autowired private WebDavService webDavService; @Autowired private FixedRuleProperties fixedRuleProperties; @Autowired private WebDavProperties webDavProperties; @Autowired private SfApiProperties sfApiProperties; @Override public String getActionCode() { return RetryActionEnum.SF_DOWNLOAD_POD.getCode(); } @Override public void handle(ApiRetryTaskEntity task, LogisticsOrderEntity order) throws Exception { // 1. 获取原始密文数据 PodPushLogEntity podLog = podPushLogService.lambdaQuery() .eq(PodPushLogEntity::getWaybillNo, order.getSfWaybillNo()) .orderByDesc(PodPushLogEntity::getId) .last("LIMIT 1").one(); if (podLog == null || StrUtil.isBlank(podLog.getRawPushData())) { throw new RuntimeException("流水表中未发现加密的回单数据,等待顺丰推送..."); } log.info(">>> 任务执行:开始解密单号 {} 的原始报文", order.getOrderNo()); // 2. 执行解密逻辑 (AES解密 + 二次解码) byte[] finalPdfBytes; try { // 获取并处理 Key (CheckWord 前16位) // String secret = "b283e485aad2a78f"; String secret = sfApiProperties.getPodSecret(); if (StrUtil.isBlank(secret)) { secret = "axjGikuWgvYkI3JA"; // 沙箱 } if (secret.length() > 16) { secret = secret.substring(0, 16); } finalPdfBytes = decryptAndDecode2(podLog.getRawPushData(), secret); } catch (Exception e) { log.error(">>> 解密失败: {}", e.getMessage()); throw new RuntimeException("DECRYPT_PROCESS_FAILED: " + e.getMessage()); } // 3. 上传 WebDAV String datePath = FilePathUtil.getHierarchicalPath(new Date()); String fileName = order.getOrderNo() + ".pdf"; String fullSavePath = webDavProperties.getPodRoot() + "/" + datePath + "/" + fileName; webDavService.uploadFile(fullSavePath , finalPdfBytes); // 4. 更新订单和日志状态 order.setPodPdfPath(fullSavePath); order.setOrderStatus(OrderStatusEnum.FINISHED.getCode()); // 流程结束 logisticsOrderService.updateById(order); podLog.setProcessStatus(1); // 成功 podPushLogService.updateById(podLog); log.info(">>> 回单处理成功并已上传 WebDAV: {}", fullSavePath); } /** * 内部解密方法:AES -> Base64解码 */ private byte[] decryptAndDecode(String rawEncryptedBase64, String secret) throws Exception { // A. 第一次解码:密文Base64 -> 密文字节 byte[] cipherText = Base64.getDecoder().decode(rawEncryptedBase64.trim().replaceAll("\\s+", "")); // B. AES 解密 SecretKeySpec skeySpec = new SecretKeySpec(secret.getBytes("UTF-8"), "AES"); IvParameterSpec iv = new IvParameterSpec(new byte[16]); // 16个0 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv); byte[] decryptedBytes = cipher.doFinal(cipherText); // C. 第二次解码:顺丰解密后的字节其实是 PDF 的 Base64 字符串 String pdfBase64Str = new String(decryptedBytes, "UTF-8").trim(); return Base64.getDecoder().decode(pdfBase64Str); } public static byte[] ivBytes = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; private byte[] decryptAndDecode2(String rawEncryptedBase64, String secret) throws Exception { // 1. 密钥深度清理:去空格、确保16位 String cleanedSecret = secret.trim(); if (cleanedSecret.length() > 16) { cleanedSecret = cleanedSecret.substring(0, 16); } byte[] keyBytes = cleanedSecret.getBytes(StandardCharsets.UTF_8); // 2. 密文深度清理:去除所有换行、回车、空格 // 关键点:Standard Decoder 遇到换行会出问题,必须手动清理或使用 MimeDecoder String cleanedPayload = rawEncryptedBase64.trim().replaceAll("\\s", ""); // 3. 使用 MimeDecoder (更稳健,会自动忽略非法字符) byte[] encryptedBytes = Base64.getMimeDecoder().decode(cleanedPayload); // 4. AES 解密配置 SecretKeySpec newKey = new SecretKeySpec(keyBytes, "AES"); // 顺丰 POD 推送固定使用 16 个 0 字节作为 IV IvParameterSpec ivSpec = new IvParameterSpec(new byte[16]); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, newKey, ivSpec); byte[] decryptedResult; try { log.info("DEBUG: 待解密字节数组长度: {}", encryptedBytes.length); decryptedResult = cipher.doFinal(encryptedBytes); } catch (Exception e) { log.error("AES解密失败!密钥长度: {}, 密文字节长度: {}", keyBytes.length, encryptedBytes.length); throw e; } // 5. 顺丰解出的是 PDF 的 Base64 文本,需要进行二次解码 String pdfBase64Str = new String(decryptedResult, StandardCharsets.UTF_8).trim().replaceAll("\\s", ""); return Base64.getMimeDecoder().decode(pdfBase64Str); } }