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
165 lines
6.8 KiB
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);
|
|
}
|
|
}
|