提交 394a4e97 authored 作者: lidongxu's avatar lidongxu

修改手机号后4位加SF运单号查询顺丰物流信息

上级 b26415c4
......@@ -85,8 +85,8 @@ public class SentQueryController {
private List<OrderSentInfoResponse> sf(OrdersSentDto sent) {
// getOrderTrace 返回 routeResps 数组,每个元素含 mailNo + routes
// 使用 ddNo(客户订单号)查询,trackingType=2
JSONArray routeResps = sfUtil.getOrderTrace(sent.getDdNo());
// 使用 expressNo(顺丰运单号)+ lastFourPhoneNumber 查询,trackingType=1
JSONArray routeResps = sfUtil.getOrderTrace(sent.getExpressNo(), sent.getLastFourPhoneNumber());
if (CollectionUtils.isEmpty(routeResps)) {
return null;
}
......
......@@ -95,6 +95,11 @@ public class OrdersSentDto implements Serializable {
*/
private String receiptPhotoCompleteFlag;
/**
* 收件人手机号后四位(顺丰路由查询校验用)
*/
private String lastFourPhoneNumber;
private List<OrderSentInfoResponse> sentInfo;
......
......@@ -5,66 +5,76 @@ import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.sfa.common.core.enums.ECode;
import com.sfa.common.core.exception.ServiceException;
import com.sfa.common.redis.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author : lidongxu
* @date : 2025-02-05
* @describe : 顺丰物流工具类
* @describe : 顺丰物流工具类(OAuth2 accessToken 认证)
*/
@Slf4j
@Component
public class SfUtil {
private static final String SF_ACCESS_TOKEN_CACHE_KEY = "sf:access_token";
private static final String SF_OAUTH_URL = "https://sfapi.sf-express.com/oauth2/accessToken";
@Value("${sf.api_url}")
private String sfApiUrl;
@Value("${sf.partner_id}")
private String partnerId;
@Value("${sf.check_word}")
private String checkWord;
@Value("${sf.secret}")
private String secret;
@Autowired
private RedisService redisService;
/**
* 查询顺丰物流轨迹(根据客户订单号查询
* 查询顺丰物流轨迹(根据顺丰运单号查询,trackingType=1
*
* @param ddNo 客户订单号(DD单号)
* @param expressNo 顺丰运单号
* @param lastFourPhoneNumber 收件人手机号后四位
* @return routeResps 列表(每个元素含 mailNo + routes)
*/
public JSONArray getOrderTrace(String ddNo) {
public JSONArray getOrderTrace(String expressNo, String lastFourPhoneNumber) {
try {
String accessToken = getAccessToken();
long timestamp = System.currentTimeMillis();
// 构建 msgData(trackingType=2 根据客户订单号查询,trackingNumber 必须为 List)
// 构建 msgData
Map<String, Object> bizData = new HashMap<>();
bizData.put("language", "zh-CN");
bizData.put("trackingType", "2");
bizData.put("trackingNumber", Collections.singletonList(ddNo));
bizData.put("methodType", "1");
bizData.put("trackingType", 1);
bizData.put("trackingNumber", Collections.singletonList(expressNo));
bizData.put("checkPhoneNo", lastFourPhoneNumber);
String msgData = JSONObject.toJSONString(bizData);
// 构建公共请求参数(保持顺序,便于排查)
// 构建表单参数
Map<String, String> requestBody = new LinkedHashMap<>();
requestBody.put("partnerID", partnerId);
requestBody.put("requestID", String.valueOf(timestamp));
requestBody.put("requestID", UUID.randomUUID().toString());
requestBody.put("serviceCode", "EXP_RECE_SEARCH_ROUTES");
requestBody.put("timestamp", String.valueOf(timestamp));
requestBody.put("accessToken", accessToken);
requestBody.put("msgData", msgData);
requestBody.put("msgDigest", generateMsgDigest(msgData, String.valueOf(timestamp)));
// 手动 URL 编码,确保顺丰服务端能正确解析
// 手动 URL 编码
StringBuilder bodyBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : requestBody.entrySet()) {
if (bodyBuilder.length() > 0) bodyBuilder.append("&");
......@@ -76,6 +86,7 @@ public class SfUtil {
log.info("顺丰路由查询请求体:{}", bodyBuilder);
String response = HttpUtil.createPost(sfApiUrl)
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
.body(bodyBuilder.toString())
.timeout(15000)
......@@ -99,7 +110,7 @@ public class SfUtil {
}
JSONObject apiResultData = JSONObject.parseObject(apiResultDataStr);
// 内层业务成功码:S0000
// 内层业务成功标志
if (!Boolean.TRUE.equals(apiResultData.getBoolean("success"))) {
log.error("顺丰路由查询业务返回失败:{}", apiResultDataStr);
throw new ServiceException(ECode.SF_ORDER_TRACE_API_ERROR);
......@@ -122,16 +133,41 @@ public class SfUtil {
}
/**
* 简易 MD5 数字签名:Base64( MD5( msgData + timestamp + checkWord ) )
* 获取顺丰 OAuth2 accessToken,优先从 Redis 缓存读取
*/
private String generateMsgDigest(String msgData, String timestamp) {
private String getAccessToken() {
String cached = redisService.getCacheObject(SF_ACCESS_TOKEN_CACHE_KEY);
if (cached != null) {
return cached;
}
try {
String signStr = msgData + timestamp + checkWord;
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(signStr.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(digest);
String response = HttpUtil.createPost(SF_OAUTH_URL)
.header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
.form("partnerID", partnerId)
.form("secret", secret)
.form("grantType", "password")
.timeout(10000)
.execute()
.body();
log.info("顺丰获取 accessToken 响应:{}", response);
JSONObject json = JSONObject.parseObject(response);
if (!"A1000".equals(json.getString("apiResultCode"))) {
log.error("顺丰获取 accessToken 失败:{}", response);
throw new ServiceException(ECode.SF_SIGN_ERROR);
}
String accessToken = json.getString("accessToken");
// expiresIn 单位为秒,提前 60 秒过期,避免临界问题
long expiresIn = json.getLongValue("expiresIn") - 60;
redisService.setCacheObject(SF_ACCESS_TOKEN_CACHE_KEY, accessToken, expiresIn, TimeUnit.SECONDS);
return accessToken;
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("生成顺丰签名失败", e);
log.error("获取顺丰 accessToken 异常:{}", e.getMessage(), e);
throw new ServiceException(ECode.SF_SIGN_ERROR);
}
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论