Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
W
wangxiaolu-sfa-module-job
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
sfa
wangxiaolu-sfa-module-job
Commits
394a4e97
提交
394a4e97
authored
3月 21, 2026
作者:
lidongxu
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
修改手机号后4位加SF运单号查询顺丰物流信息
上级
b26415c4
隐藏空白字符变更
内嵌
并排
正在显示
3 个修改的文件
包含
67 行增加
和
26 行删除
+67
-26
SentQueryController.java
...ava/com/sfa/job/controller/order/SentQueryController.java
+2
-2
OrdersSentDto.java
src/main/java/com/sfa/job/pojo/response/OrdersSentDto.java
+5
-0
SfUtil.java
src/main/java/com/sfa/job/util/SfUtil.java
+60
-24
没有找到文件。
src/main/java/com/sfa/job/controller/order/SentQueryController.java
浏览文件 @
394a4e97
...
@@ -85,8 +85,8 @@ public class SentQueryController {
...
@@ -85,8 +85,8 @@ public class SentQueryController {
private
List
<
OrderSentInfoResponse
>
sf
(
OrdersSentDto
sent
)
{
private
List
<
OrderSentInfoResponse
>
sf
(
OrdersSentDto
sent
)
{
// getOrderTrace 返回 routeResps 数组,每个元素含 mailNo + routes
// getOrderTrace 返回 routeResps 数组,每个元素含 mailNo + routes
// 使用
ddNo(客户订单号)查询,trackingType=2
// 使用
expressNo(顺丰运单号)+ lastFourPhoneNumber 查询,trackingType=1
JSONArray
routeResps
=
sfUtil
.
getOrderTrace
(
sent
.
get
DdNo
());
JSONArray
routeResps
=
sfUtil
.
getOrderTrace
(
sent
.
get
ExpressNo
(),
sent
.
getLastFourPhoneNumber
());
if
(
CollectionUtils
.
isEmpty
(
routeResps
))
{
if
(
CollectionUtils
.
isEmpty
(
routeResps
))
{
return
null
;
return
null
;
}
}
...
...
src/main/java/com/sfa/job/pojo/response/OrdersSentDto.java
浏览文件 @
394a4e97
...
@@ -95,6 +95,11 @@ public class OrdersSentDto implements Serializable {
...
@@ -95,6 +95,11 @@ public class OrdersSentDto implements Serializable {
*/
*/
private
String
receiptPhotoCompleteFlag
;
private
String
receiptPhotoCompleteFlag
;
/**
* 收件人手机号后四位(顺丰路由查询校验用)
*/
private
String
lastFourPhoneNumber
;
private
List
<
OrderSentInfoResponse
>
sentInfo
;
private
List
<
OrderSentInfoResponse
>
sentInfo
;
...
...
src/main/java/com/sfa/job/util/SfUtil.java
浏览文件 @
394a4e97
...
@@ -5,66 +5,76 @@ import com.alibaba.fastjson2.JSONArray;
...
@@ -5,66 +5,76 @@ import com.alibaba.fastjson2.JSONArray;
import
com.alibaba.fastjson2.JSONObject
;
import
com.alibaba.fastjson2.JSONObject
;
import
com.sfa.common.core.enums.ECode
;
import
com.sfa.common.core.enums.ECode
;
import
com.sfa.common.core.exception.ServiceException
;
import
com.sfa.common.core.exception.ServiceException
;
import
com.sfa.common.redis.service.RedisService
;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.beans.factory.annotation.Value
;
import
org.springframework.stereotype.Component
;
import
org.springframework.stereotype.Component
;
import
java.net.URLEncoder
;
import
java.net.URLEncoder
;
import
java.nio.charset.StandardCharsets
;
import
java.nio.charset.StandardCharsets
;
import
java.security.MessageDigest
;
import
java.util.Base64
;
import
java.util.Collections
;
import
java.util.Collections
;
import
java.util.HashMap
;
import
java.util.HashMap
;
import
java.util.LinkedHashMap
;
import
java.util.LinkedHashMap
;
import
java.util.Map
;
import
java.util.Map
;
import
java.util.UUID
;
import
java.util.concurrent.TimeUnit
;
/**
/**
* @author : lidongxu
* @author : lidongxu
* @date : 2025-02-05
* @date : 2025-02-05
* @describe : 顺丰物流工具类
* @describe : 顺丰物流工具类
(OAuth2 accessToken 认证)
*/
*/
@Slf4j
@Slf4j
@Component
@Component
public
class
SfUtil
{
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}"
)
@Value
(
"${sf.api_url}"
)
private
String
sfApiUrl
;
private
String
sfApiUrl
;
@Value
(
"${sf.partner_id}"
)
@Value
(
"${sf.partner_id}"
)
private
String
partnerId
;
private
String
partnerId
;
@Value
(
"${sf.check_word}"
)
@Value
(
"${sf.secret}"
)
private
String
checkWord
;
private
String
secret
;
@Autowired
private
RedisService
redisService
;
/**
/**
* 查询顺丰物流轨迹(根据
客户订单号查询
)
* 查询顺丰物流轨迹(根据
顺丰运单号查询,trackingType=1
)
*
*
* @param ddNo 客户订单号(DD单号)
* @param expressNo 顺丰运单号
* @param lastFourPhoneNumber 收件人手机号后四位
* @return routeResps 列表(每个元素含 mailNo + routes)
* @return routeResps 列表(每个元素含 mailNo + routes)
*/
*/
public
JSONArray
getOrderTrace
(
String
ddNo
)
{
public
JSONArray
getOrderTrace
(
String
expressNo
,
String
lastFourPhoneNumber
)
{
try
{
try
{
String
accessToken
=
getAccessToken
();
long
timestamp
=
System
.
currentTimeMillis
();
long
timestamp
=
System
.
currentTimeMillis
();
// 构建 msgData
(trackingType=2 根据客户订单号查询,trackingNumber 必须为 List)
// 构建 msgData
Map
<
String
,
Object
>
bizData
=
new
HashMap
<>();
Map
<
String
,
Object
>
bizData
=
new
HashMap
<>();
bizData
.
put
(
"language"
,
"zh-CN"
);
bizData
.
put
(
"language"
,
"zh-CN"
);
bizData
.
put
(
"trackingType"
,
"2"
);
bizData
.
put
(
"trackingType"
,
1
);
bizData
.
put
(
"trackingNumber"
,
Collections
.
singletonList
(
dd
No
));
bizData
.
put
(
"trackingNumber"
,
Collections
.
singletonList
(
express
No
));
bizData
.
put
(
"
methodType"
,
"1"
);
bizData
.
put
(
"
checkPhoneNo"
,
lastFourPhoneNumber
);
String
msgData
=
JSONObject
.
toJSONString
(
bizData
);
String
msgData
=
JSONObject
.
toJSONString
(
bizData
);
// 构建
公共请求参数(保持顺序,便于排查)
// 构建
表单参数
Map
<
String
,
String
>
requestBody
=
new
LinkedHashMap
<>();
Map
<
String
,
String
>
requestBody
=
new
LinkedHashMap
<>();
requestBody
.
put
(
"partnerID"
,
partnerId
);
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
(
"serviceCode"
,
"EXP_RECE_SEARCH_ROUTES"
);
requestBody
.
put
(
"timestamp"
,
String
.
valueOf
(
timestamp
));
requestBody
.
put
(
"timestamp"
,
String
.
valueOf
(
timestamp
));
requestBody
.
put
(
"accessToken"
,
accessToken
);
requestBody
.
put
(
"msgData"
,
msgData
);
requestBody
.
put
(
"msgData"
,
msgData
);
requestBody
.
put
(
"msgDigest"
,
generateMsgDigest
(
msgData
,
String
.
valueOf
(
timestamp
)));
// 手动 URL 编码
,确保顺丰服务端能正确解析
// 手动 URL 编码
StringBuilder
bodyBuilder
=
new
StringBuilder
();
StringBuilder
bodyBuilder
=
new
StringBuilder
();
for
(
Map
.
Entry
<
String
,
String
>
entry
:
requestBody
.
entrySet
())
{
for
(
Map
.
Entry
<
String
,
String
>
entry
:
requestBody
.
entrySet
())
{
if
(
bodyBuilder
.
length
()
>
0
)
bodyBuilder
.
append
(
"&"
);
if
(
bodyBuilder
.
length
()
>
0
)
bodyBuilder
.
append
(
"&"
);
...
@@ -76,6 +86,7 @@ public class SfUtil {
...
@@ -76,6 +86,7 @@ public class SfUtil {
log
.
info
(
"顺丰路由查询请求体:{}"
,
bodyBuilder
);
log
.
info
(
"顺丰路由查询请求体:{}"
,
bodyBuilder
);
String
response
=
HttpUtil
.
createPost
(
sfApiUrl
)
String
response
=
HttpUtil
.
createPost
(
sfApiUrl
)
.
header
(
"Authorization"
,
"Bearer "
+
accessToken
)
.
header
(
"Content-Type"
,
"application/x-www-form-urlencoded;charset=UTF-8"
)
.
header
(
"Content-Type"
,
"application/x-www-form-urlencoded;charset=UTF-8"
)
.
body
(
bodyBuilder
.
toString
())
.
body
(
bodyBuilder
.
toString
())
.
timeout
(
15000
)
.
timeout
(
15000
)
...
@@ -99,7 +110,7 @@ public class SfUtil {
...
@@ -99,7 +110,7 @@ public class SfUtil {
}
}
JSONObject
apiResultData
=
JSONObject
.
parseObject
(
apiResultDataStr
);
JSONObject
apiResultData
=
JSONObject
.
parseObject
(
apiResultDataStr
);
// 内层业务成功
码:S0000
// 内层业务成功
标志
if
(!
Boolean
.
TRUE
.
equals
(
apiResultData
.
getBoolean
(
"success"
)))
{
if
(!
Boolean
.
TRUE
.
equals
(
apiResultData
.
getBoolean
(
"success"
)))
{
log
.
error
(
"顺丰路由查询业务返回失败:{}"
,
apiResultDataStr
);
log
.
error
(
"顺丰路由查询业务返回失败:{}"
,
apiResultDataStr
);
throw
new
ServiceException
(
ECode
.
SF_ORDER_TRACE_API_ERROR
);
throw
new
ServiceException
(
ECode
.
SF_ORDER_TRACE_API_ERROR
);
...
@@ -122,16 +133,41 @@ public class SfUtil {
...
@@ -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
{
try
{
String
signStr
=
msgData
+
timestamp
+
checkWord
;
String
response
=
HttpUtil
.
createPost
(
SF_OAUTH_URL
)
MessageDigest
md
=
MessageDigest
.
getInstance
(
"MD5"
);
.
header
(
"Content-Type"
,
"application/x-www-form-urlencoded;charset=UTF-8"
)
byte
[]
digest
=
md
.
digest
(
signStr
.
getBytes
(
StandardCharsets
.
UTF_8
));
.
form
(
"partnerID"
,
partnerId
)
return
Base64
.
getEncoder
().
encodeToString
(
digest
);
.
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
)
{
}
catch
(
Exception
e
)
{
log
.
error
(
"
生成顺丰签名失败"
,
e
);
log
.
error
(
"
获取顺丰 accessToken 异常:{}"
,
e
.
getMessage
()
,
e
);
throw
new
ServiceException
(
ECode
.
SF_SIGN_ERROR
);
throw
new
ServiceException
(
ECode
.
SF_SIGN_ERROR
);
}
}
}
}
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论