提交 28978247 authored 作者: lidongxu's avatar lidongxu

尝试添加顺丰查询物流接口

上级 e930640e
......@@ -11,6 +11,7 @@ import com.sfa.job.pojo.response.OrdersSentDto;
import com.sfa.job.service.order.IOrdersSentQueryService;
import com.sfa.job.util.JdtcUtil;
import com.sfa.job.util.KyeUtil;
import com.sfa.job.util.SfUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
......@@ -19,10 +20,8 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author : liqiulin
* @date : 2025-07-08 13
......@@ -31,31 +30,39 @@ import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/sent")
public class SentQueryController {
@Autowired
private JdtcUtil jdtcUtil;
@Autowired
private KyeUtil kyeUtil;
@Autowired
private SfUtil sfUtil;
@Autowired
private IOrdersSentQueryService orderSentQueryService;
@Autowired
private RedisService redisService;
/**
* 接口:GET /sent/query_p?sentNo=xxx
* 限频:每小时同一单号最多查询 5 次
*/
@GetMapping("/query_p")
public Object query(String sentNo){
// 限制单号每小时只能查询5次
public Object query(String sentNo) {
String rKey = RedisKeyJob.QINCE_ORDER_SENT_INTERNET_COUNT + sentNo;
Object cacheObject = redisService.getCacheObject(rKey);
if (cacheObject != null && Integer.parseInt(cacheObject.toString()) >= 5) {
throw new ServiceException(ECode.SENT_NO_QUERY_COUNT_ERROR);
}
Object sent = queryBySentNode(sentNo);
redisService.setCacheObject(rKey, cacheObject == null ? 1 : Integer.parseInt(cacheObject.toString()) + 1,1L, TimeUnit.HOURS);
redisService.setCacheObject(rKey,
cacheObject == null ? 1 : Integer.parseInt(cacheObject.toString()) + 1,
1L, TimeUnit.HOURS);
return sent;
}
private Object queryBySentNode(String sentNo){
private Object queryBySentNode(String sentNo) {
OrdersSentDto sent = orderSentQueryService.getSent(sentNo);
List<OrderSentInfoResponse> sentInfo = null;
List<OrderSentInfoResponse> sentInfo;
switch (sent.getTransport()) {
case "134":
sentInfo = jdTC134(sent);
......@@ -63,6 +70,10 @@ public class SentQueryController {
case "109":
sentInfo = kye109(sent);
break;
case "136": // 顺丰干配
case "117": // 顺丰
sentInfo = sf(sent);
break;
default:
throw new ServiceException(ECode.SENT_ISNULL_ERROR);
}
......@@ -70,40 +81,79 @@ public class SentQueryController {
return sent;
}
private List<OrderSentInfoResponse> kye109(OrdersSentDto sent) {
JSONArray traces = kyeUtil.getOrderTrace(sent.getExpressNo());
if (CollectionUtils.isEmpty(traces)){
// =================== 顺丰 ===================
private List<OrderSentInfoResponse> sf(OrdersSentDto sent) {
// getOrderTrace 返回 routeResps 数组,每个元素含 mailNo + routes
JSONArray routeResps = sfUtil.getOrderTrace(sent.getExpressNo());
if (CollectionUtils.isEmpty(routeResps)) {
return null;
}
JSONArray exteriorRouteList = traces.getJSONObject(0).getJSONArray("exteriorRouteList");
return pKye109(exteriorRouteList);
// 取第一个运单的 routes 列表
JSONObject routeResp = routeResps.getJSONObject(0);
JSONArray routes = routeResp.getJSONArray("routes");
if (CollectionUtils.isEmpty(routes)) {
return null;
}
return pSf(routes);
}
private List<OrderSentInfoResponse> pSf(JSONArray routes) {
List<OrderSentInfoResponse> sentInfoList = new ArrayList<>();
routes.forEach(route -> {
JSONObject r = (JSONObject) route;
OrderSentInfoResponse sentInfo = new OrderSentInfoResponse();
sentInfo.setOperateTime(r.getString("acceptTime"));
sentInfo.setOperateRemark(r.getString("remark"));
sentInfo.setAcceptAddress(r.getString("acceptAddress"));
sentInfo.setOpCode(r.getString("opCode"));
sentInfo.setFirstStatusCode(r.getString("firstStatusCode"));
sentInfo.setFirstStatusName(r.getString("firstStatusName"));
sentInfo.setSecondaryStatusCode(r.getString("secondaryStatusCode"));
sentInfo.setSecondaryStatusName(r.getString("secondaryStatusName"));
sentInfoList.add(sentInfo);
});
return sentInfoList;
}
private List<OrderSentInfoResponse> jdTC134(OrdersSentDto sent){
// =================== 京东 ===================
private List<OrderSentInfoResponse> jdTC134(OrdersSentDto sent) {
JSONArray traces = jdtcUtil.getOrderTrace(sent.getBjSentNo() + "-" + sent.getBjSentVersion());
JSONObject jb = traces.getJSONObject(0);
return pJdTC134(jb.getJSONArray("traceDetails"));
}
private List<OrderSentInfoResponse> pJdTC134(JSONArray exteriorRouteList){
private List<OrderSentInfoResponse> pJdTC134(JSONArray exteriorRouteList) {
List<OrderSentInfoResponse> sentInfoList = new ArrayList<>();
exteriorRouteList.forEach(exteriorRoute -> {
JSONObject exteriorRouteJson = (JSONObject) exteriorRoute;
JSONObject r = (JSONObject) exteriorRoute;
OrderSentInfoResponse sentInfo = new OrderSentInfoResponse();
sentInfo.setOperateTime(exteriorRouteJson.getString("operateTime"));
sentInfo.setOperateRemark(exteriorRouteJson.getString("operateRemark"));
sentInfo.setOperateTime(r.getString("operateTime"));
sentInfo.setOperateRemark(r.getString("operateRemark"));
sentInfoList.add(sentInfo);
});
return sentInfoList;
}
private List<OrderSentInfoResponse> pKye109(JSONArray exteriorRouteList){
// =================== 快鱼 ===================
private List<OrderSentInfoResponse> kye109(OrdersSentDto sent) {
JSONArray traces = kyeUtil.getOrderTrace(sent.getExpressNo());
if (CollectionUtils.isEmpty(traces)) {
return null;
}
JSONArray exteriorRouteList = traces.getJSONObject(0).getJSONArray("exteriorRouteList");
return pKye109(exteriorRouteList);
}
private List<OrderSentInfoResponse> pKye109(JSONArray exteriorRouteList) {
List<OrderSentInfoResponse> sentInfoList = new ArrayList<>();
exteriorRouteList.forEach(exteriorRoute -> {
JSONObject exteriorRouteJson = (JSONObject) exteriorRoute;
JSONObject r = (JSONObject) exteriorRoute;
OrderSentInfoResponse sentInfo = new OrderSentInfoResponse();
sentInfo.setOperateTime(exteriorRouteJson.getString("uploadDate"));
sentInfo.setOperateRemark(exteriorRouteJson.getString("routeDescription"));
sentInfo.setOperateTime(r.getString("uploadDate"));
sentInfo.setOperateRemark(r.getString("routeDescription"));
sentInfoList.add(sentInfo);
});
return sentInfoList;
......
......@@ -5,18 +5,32 @@ import lombok.Data;
/**
* @author : liqiulin
* @date : 2025-07-30 15
* @describe :
* @describe : 统一物流路由节点响应 DTO
*/
@Data
public class OrderSentInfoResponse {
/**
* 路由节点描述
*/
/** 路由节点时间(格式:YYYY-MM-DD HH24:MM:SS) */
private String operateTime;
/** 路由节点描述 */
private String operateRemark;
/**
* 路由节点时间
*/
private String operateTime;
/** 路由节点发生地点(顺丰) */
private String acceptAddress;
/** 路由节点操作码(顺丰) */
private String opCode;
/** 一级状态编码(顺丰) */
private String firstStatusCode;
/** 一级状态名称(顺丰) */
private String firstStatusName;
/** 二级状态编码(顺丰) */
private String secondaryStatusCode;
/** 二级状态名称(顺丰) */
private String secondaryStatusName;
}
package com.sfa.job.util;
import cn.hutool.http.HttpUtil;
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 lombok.extern.slf4j.Slf4j;
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;
/**
* @author : lidongxu
* @date : 2025-02-05
* @describe : 顺丰物流工具类
*/
@Slf4j
@Component
public class SfUtil {
@Value("${sf.api_url}")
private String sfApiUrl;
@Value("${sf.partner_id}")
private String partnerId;
@Value("${sf.check_word}")
private String checkWord;
/**
* 查询顺丰物流轨迹
*
* @param expressNo 顺丰运单号
* @return routeResps 列表(每个元素含 mailNo + routes)
*/
public JSONArray getOrderTrace(String expressNo) {
try {
long timestamp = System.currentTimeMillis();
// 构建 msgData(trackingNumber 必须为 List)
Map<String, Object> bizData = new HashMap<>();
bizData.put("language", "zh-CN");
bizData.put("trackingType", "1");
bizData.put("trackingNumber", Collections.singletonList(expressNo));
bizData.put("methodType", "1");
String msgData = JSONObject.toJSONString(bizData);
// 构建公共请求参数(保持顺序,便于排查)
Map<String, String> requestBody = new LinkedHashMap<>();
requestBody.put("partnerID", partnerId);
requestBody.put("requestID", String.valueOf(timestamp));
requestBody.put("serviceCode", "EXP_RECE_SEARCH_ROUTES");
requestBody.put("timestamp", String.valueOf(timestamp));
requestBody.put("msgData", msgData);
requestBody.put("msgDigest", generateMsgDigest(msgData, String.valueOf(timestamp)));
// 手动 URL 编码,确保顺丰服务端能正确解析
StringBuilder bodyBuilder = new StringBuilder();
for (Map.Entry<String, String> entry : requestBody.entrySet()) {
if (bodyBuilder.length() > 0) bodyBuilder.append("&");
bodyBuilder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()))
.append("=")
.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()));
}
log.info("顺丰路由查询请求体:{}", bodyBuilder);
String response = HttpUtil.createPost(sfApiUrl)
.header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
.body(bodyBuilder.toString())
.timeout(15000)
.execute()
.body();
log.info("顺丰路由查询响应:{}", response);
JSONObject responseJson = JSONObject.parseObject(response);
// 外层成功码:A1000
if (!"A1000".equals(responseJson.getString("apiResultCode"))) {
log.error("顺丰路由查询接口返回异常:{}", response);
throw new ServiceException(ECode.SF_ORDER_TRACE_API_ERROR);
}
// apiResultData 是 String,需要再次 parse
String apiResultDataStr = responseJson.getString("apiResultData");
if (apiResultDataStr == null) {
return new JSONArray();
}
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);
}
JSONObject msgDataResult = apiResultData.getJSONObject("msgData");
if (msgDataResult == null) {
return new JSONArray();
}
JSONArray routeResps = msgDataResult.getJSONArray("routeResps");
return routeResps != null ? routeResps : new JSONArray();
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("查询顺丰物流轨迹异常:{}", e.getMessage(), e);
throw new ServiceException(ECode.SF_ORDER_TRACE_QUERY_ERROR);
}
}
/**
* 简易 MD5 数字签名:Base64( MD5( msgData + timestamp + checkWord ) )
*/
private String generateMsgDigest(String msgData, String timestamp) {
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);
} catch (Exception e) {
log.error("生成顺丰签名失败", e);
throw new ServiceException(ECode.SF_SIGN_ERROR);
}
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论