no message

This commit is contained in:
cb 2025-07-31 16:34:53 +08:00
parent 671a9f225e
commit 1fe53780b9
5 changed files with 284 additions and 84 deletions

View File

@ -147,12 +147,22 @@ aliyun:
authCode: od2FgE9a9g
# 物流API配置
logistics:
kdniao:
# 快递鸟 API配置
appId: '1889454' # 快递鸟应用ID需要到快递鸟官网申请
appKey: 'b2483529-807d-49af-b0e1-1ed218baa4db' # 快递鸟API密钥需要到快递鸟官网申请
url: 'https://api.kdniao.com/api/dist' # 快递鸟即时查询接口地址
# logistics:
# kdniao:
# # 快递鸟 API配置
# appId: '1889454' # 快递鸟应用ID需要到快递鸟官网申请
# appKey: 'b2483529-807d-49af-b0e1-1ed218baa4db' # 快递鸟API密钥需要到快递鸟官网申请
# url: 'https://api.kdniao.com/api/dist' # 快递鸟即时查询接口地址
# 快递鸟支持两种接口:
# 1. 即时查询接口https://api.kdniao.com/api/dist
# 2. 物流跟踪接口https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx
# 2. 物流跟踪接口https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx
logistics:
# 阿里云快递查询API配置
aliyun:
# 阿里云API网关AppCode需要在阿里云市场购买快递查询服务后获取
appCode: "78064b8aaa294f64ab07a285f129aea6"
# API主机地址
host: "https://kzexpress.market.alicloudapi.com"
# API路径
path: "/api-mall/api/express/query"

View File

@ -27,4 +27,9 @@ public class LogisticsQueryRequest {
* 快递公司名称可选如果不传会自动识别
*/
private String expressName;
/**
* 手机号阿里云快递查询API需要
*/
private String mobile;
}

View File

@ -35,6 +35,16 @@ public interface LogisticsService {
*/
LogisticsInfo queryLogistics(String trackingNumber, String expressCode);
/**
* 根据快递单号快递公司编码和手机号查询物流信息
*
* @param trackingNumber 快递单号
* @param expressCode 快递公司编码
* @param mobile 手机号
* @return 物流信息
*/
LogisticsInfo queryLogistics(String trackingNumber, String expressCode, String mobile);
/**
* 根据订单ID查询物流信息
*

View File

@ -1,7 +1,6 @@
package com.ghy.order.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
@ -10,8 +9,11 @@ import com.ghy.order.domain.OrderMaster;
import com.ghy.order.request.LogisticsQueryRequest;
import com.ghy.order.service.LogisticsService;
import com.ghy.order.service.OrderMasterService;
import com.ghy.order.utils.HttpUtils;
import com.ghy.order.utils.LogisticsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@ -35,29 +37,41 @@ public class LogisticsServiceImpl implements LogisticsService {
private OrderMasterService orderMasterService;
/**
* 快递鸟 API配置
* 阿里云快递查询 API配置
*/
@Value("${logistics.kdniao.appId:}")
private String appId;
@Value("${logistics.aliyun.appCode:}")
private String appCode;
@Value("${logistics.kdniao.appKey:}")
private String appKey;
@Value("${logistics.aliyun.host:https://kzexpress.market.alicloudapi.com}")
private String host;
@Value("${logistics.kdniao.url:https://api.kdniao.com/api/dist}")
private String apiUrl;
@Value("${logistics.aliyun.path:/api-mall/api/express/query}")
private String path;
@Override
public LogisticsInfo queryLogistics(LogisticsQueryRequest request) {
return queryLogistics(request.getTrackingNumber(), request.getExpressCode());
return queryLogistics(request.getTrackingNumber(), request.getExpressCode(), request.getMobile());
}
@Override
public LogisticsInfo queryLogistics(String trackingNumber) {
return queryLogistics(trackingNumber, null);
return queryLogistics(trackingNumber, null, null);
}
@Override
public LogisticsInfo queryLogistics(String trackingNumber, String expressCode) {
return queryLogistics(trackingNumber, expressCode, null);
}
/**
* 根据快递单号查询物流信息阿里云API
*
* @param trackingNumber 快递单号
* @param expressCode 快递公司编码
* @param mobile 手机号
* @return 物流信息
*/
public LogisticsInfo queryLogistics(String trackingNumber, String expressCode, String mobile) {
LogisticsInfo logisticsInfo = new LogisticsInfo();
logisticsInfo.setTrackingNumber(trackingNumber);
logisticsInfo.setQueryTime(new Date());
@ -70,41 +84,47 @@ public class LogisticsServiceImpl implements LogisticsService {
return logisticsInfo;
}
// 如果没有指定快递公司先识别快递公司
if (StrUtil.isBlank(expressCode)) {
expressCode = LogisticsUtils.identifyExpressCompany(trackingNumber);
}
// 调用阿里云快递查询 API
JSONObject result = callAliyunAPI(trackingNumber, mobile);
// 调用快递鸟 API查询物流信息
JSONObject result = callKdniaoAPI(trackingNumber, expressCode);
if (result.getBool("Success", false)) {
if (result.getBool("success", false)) {
// 查询成功
logisticsInfo.setSuccess(true);
logisticsInfo.setExpressCode(expressCode);
logisticsInfo.setExpressName(result.getStr("ShipperName"));
logisticsInfo.setStatus(getKdniaoStatus(result.getStr("State")));
logisticsInfo.setStatusDesc(result.getStr("State"));
// 解析物流轨迹
List<LogisticsInfo.LogisticsTrace> traces = new ArrayList<>();
JSONArray tracesArray = result.getJSONArray("Traces");
if (tracesArray != null) {
for (int i = 0; i < tracesArray.size(); i++) {
JSONObject trace = tracesArray.getJSONObject(i);
LogisticsInfo.LogisticsTrace logisticsTrace = new LogisticsInfo.LogisticsTrace();
logisticsTrace.setTime(trace.getDate("AcceptTime"));
logisticsTrace.setLocation(trace.getStr("AcceptStation"));
logisticsTrace.setDescription(trace.getStr("AcceptStation"));
logisticsTrace.setStatus(trace.getStr("Remark"));
traces.add(logisticsTrace);
// 解析快递公司信息
JSONObject data = result.getJSONObject("data");
if (data != null) {
logisticsInfo.setExpressCode(data.getStr("cpCode"));
logisticsInfo.setExpressName(data.getStr("logisticsCompanyName"));
logisticsInfo.setStatus(getAliyunStatus(data.getStr("logisticsStatusDesc")));
logisticsInfo.setStatusDesc(data.getStr("logisticsStatusDesc"));
// 解析物流轨迹
List<LogisticsInfo.LogisticsTrace> traces = new ArrayList<>();
JSONArray tracesArray = data.getJSONArray("logisticsTraceDetailList");
if (tracesArray != null) {
for (int i = 0; i < tracesArray.size(); i++) {
JSONObject trace = tracesArray.getJSONObject(i);
LogisticsInfo.LogisticsTrace logisticsTrace = new LogisticsInfo.LogisticsTrace();
// 处理时间戳
Long timeStamp = trace.getLong("time");
if (timeStamp != null) {
logisticsTrace.setTime(new Date(timeStamp));
}
logisticsTrace.setLocation(trace.getStr("areaName"));
logisticsTrace.setDescription(trace.getStr("desc"));
logisticsTrace.setStatus(trace.getStr("logisticsStatus"));
traces.add(logisticsTrace);
}
}
logisticsInfo.setTraces(traces);
}
logisticsInfo.setTraces(traces);
} else {
// 查询失败
logisticsInfo.setSuccess(false);
logisticsInfo.setErrorMsg(result.getStr("Reason", "查询失败"));
logisticsInfo.setErrorMsg(result.getStr("msg", "查询失败"));
}
} catch (Exception e) {
@ -130,88 +150,125 @@ public class LogisticsServiceImpl implements LogisticsService {
}
/**
* 调用快递鸟 API
* 调用阿里云快递查询 API
*
* @param trackingNumber 快递单号
* @param expressCode 快递公司编码
* @param mobile 手机号
* @return API返回结果
*/
private JSONObject callKdniaoAPI(String trackingNumber, String expressCode) {
private JSONObject callAliyunAPI(String trackingNumber, String mobile) {
try {
// 构建请求数据
JSONObject requestData = new JSONObject();
requestData.set("ShipperCode", expressCode);
requestData.set("LogisticCode", trackingNumber);
String method = "GET";
String requestDataStr = requestData.toString();
// 构建请求头
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "APPCODE " + appCode);
// 计算数据签名 - 按照快递鸟官方文档
// S1: RequestData(未编码) + ApiKey
String s1 = requestDataStr + appKey;
log.info("S1: {}", s1);
// 构建查询参数
Map<String, String> querys = new HashMap<>();
querys.put("expressNo", trackingNumber);
if (StrUtil.isNotBlank(mobile)) {
querys.put("mobile", mobile);
}
// S2: MD5加密32位小写
String s2 = cn.hutool.crypto.digest.DigestUtil.md5Hex(s1);
log.info("S2: {}", s2);
log.info("阿里云快递查询API请求参数: expressNo={}, mobile={}", trackingNumber, mobile);
// S3: Base64编码
String s3 = java.util.Base64.getEncoder().encodeToString(s2.getBytes("UTF-8"));
log.info("S3: {}", s3);
// 调用API
HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);
String result = EntityUtils.toString(response.getEntity());
log.info("阿里云快递查询API响应结果: {}", result);
// S4: URL编码仅请求接口需要
String s4 = java.net.URLEncoder.encode(s3, "UTF-8");
log.info("S4: {}", s4);
// 检查响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
log.error("阿里云API返回错误状态码: {}", statusCode);
JSONObject errorResult = new JSONObject();
errorResult.set("success", false);
errorResult.set("message", "API返回错误状态码: " + statusCode);
return errorResult;
}
// 构建请求参数 - 即时查询接口使用GET请求
Map<String, Object> requestParam = new HashMap<>();
requestParam.put("RequestData", requestDataStr);
requestParam.put("EBusinessID", appId);
requestParam.put("RequestType", "8002"); // 即时查询接口
requestParam.put("DataSign", s4); // 使用URL编码后的签名
requestParam.put("DataType", "2"); // JSON格式
// 检查响应内容是否为空
if (StrUtil.isBlank(result)) {
log.error("阿里云API返回空响应");
JSONObject errorResult = new JSONObject();
errorResult.set("success", false);
errorResult.set("message", "API返回空响应");
return errorResult;
}
log.info("快递鸟API请求参数: {}", requestParam.toString());
// 检查响应内容是否以{开头JSON格式
if (!result.trim().startsWith("{")) {
log.error("阿里云API返回非JSON格式响应: {}", result.substring(0, Math.min(100, result.length())));
JSONObject errorResult = new JSONObject();
errorResult.set("success", false);
errorResult.set("message", "API返回非JSON格式响应");
return errorResult;
}
// 使用GET请求调用即时查询接口
String result = HttpUtil.get(apiUrl, requestParam);
log.info("快递鸟API响应结果: {}", result);
// 尝试解析JSON
try {
return JSONUtil.parseObj(result);
} catch (Exception jsonException) {
log.error("解析阿里云API响应JSON失败: {}", jsonException.getMessage());
JSONObject errorResult = new JSONObject();
errorResult.set("success", false);
errorResult.set("message", "解析API响应失败: " + jsonException.getMessage());
return errorResult;
}
return JSONUtil.parseObj(result);
} catch (Exception e) {
log.error("调用快递鸟API失败: {}", e.getMessage(), e);
log.error("调用阿里云快递查询API失败: {}", e.getMessage(), e);
JSONObject errorResult = new JSONObject();
errorResult.set("Success", false);
errorResult.set("Reason", "API调用失败: " + e.getMessage());
errorResult.set("success", false);
errorResult.set("message", "API调用失败: " + e.getMessage());
return errorResult;
}
}
/**
* 快递鸟状态转换为系统状态码
* 阿里云状态转换为系统状态码
*
* @param kdniaoStatus 快递鸟状态
* @param aliyunStatus 阿里云状态
* @return 系统状态码
*/
private Integer getKdniaoStatus(String kdniaoStatus) {
if (StrUtil.isBlank(kdniaoStatus)) {
private Integer getAliyunStatus(String aliyunStatus) {
if (StrUtil.isBlank(aliyunStatus)) {
return 0;
}
switch (kdniaoStatus) {
switch (aliyunStatus) {
case "在途":
case "运输中":
case "TRANSPORT":
return 0;
case "揽收":
case "已揽收":
case "ACCEPT":
return 1;
case "疑难":
case "异常":
case "EXCEPTION":
return 2;
case "签收":
case "已签收":
case "SIGN":
return 3;
case "退签":
case "已退签":
case "REJECT":
return 4;
case "派件":
case "派送中":
case "DELIVERING":
return 5;
case "退回":
case "已退回":
case "RETURN":
return 6;
case "到达驿站":
case "STA_INBOUND":
return 5; // 到达驿站也算派件状态
default:
return 0;
}

View File

@ -0,0 +1,118 @@
package com.ghy.order.utils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Http工具类 - 用于阿里云API调用
* 参考https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
*/
public class HttpUtils {
/**
* 发送GET请求
*
* @param host 主机地址
* @param path 路径
* @param method 请求方法
* @param headers 请求头
* @param querys 查询参数
* @return HttpResponse
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys) throws Exception {
HttpClient httpClient = new DefaultHttpClient();
HttpGet request = new HttpGet(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
/**
* 发送POST请求
*
* @param host 主机地址
* @param path 路径
* @param method 请求方法
* @param headers 请求头
* @param querys 查询参数
* @param bodys 请求体
* @return HttpResponse
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
Map<String, String> bodys) throws Exception {
HttpClient httpClient = new DefaultHttpClient();
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (bodys != null) {
List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
for (String key : bodys.keySet()) {
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
request.setEntity(formEntity);
}
return httpClient.execute(request);
}
/**
* 构建URL
*
* @param host 主机地址
* @param path 路径
* @param querys 查询参数
* @return 完整URL
*/
private static String buildUrl(String host, String path, Map<String, String> querys) throws Exception {
StringBuilder sbUrl = new StringBuilder();
sbUrl.append(host);
if (path != null && path.length() > 0) {
sbUrl.append(path);
}
if (null != querys) {
StringBuilder sbQuery = new StringBuilder();
for (Map.Entry<String, String> query : querys.entrySet()) {
if (0 < sbQuery.length()) {
sbQuery.append("&");
}
if (query.getKey() != null && query.getKey().length() > 0) {
sbQuery.append(query.getKey());
}
if (query.getValue() != null && query.getValue().length() > 0) {
sbQuery.append("=").append(URLEncoder.encode(query.getValue(), "utf-8"));
}
}
if (0 < sbQuery.length()) {
sbUrl.append("?").append(sbQuery);
}
}
return sbUrl.toString();
}
}