You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

165 lines
6.8 KiB

1 month ago
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);
}
}