diff --git a/ghy-admin/src/main/resources/application.yaml b/ghy-admin/src/main/resources/application.yaml index afb725a3..6cbd33f8 100644 --- a/ghy-admin/src/main/resources/application.yaml +++ b/ghy-admin/src/main/resources/application.yaml @@ -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 \ No newline at end of file + # 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" \ No newline at end of file diff --git a/ghy-order/src/main/java/com/ghy/order/request/LogisticsQueryRequest.java b/ghy-order/src/main/java/com/ghy/order/request/LogisticsQueryRequest.java index 40a0fc6b..db3e01a6 100644 --- a/ghy-order/src/main/java/com/ghy/order/request/LogisticsQueryRequest.java +++ b/ghy-order/src/main/java/com/ghy/order/request/LogisticsQueryRequest.java @@ -27,4 +27,9 @@ public class LogisticsQueryRequest { * 快递公司名称(可选,如果不传会自动识别) */ private String expressName; + + /** + * 手机号(阿里云快递查询API需要) + */ + private String mobile; } \ No newline at end of file diff --git a/ghy-order/src/main/java/com/ghy/order/service/LogisticsService.java b/ghy-order/src/main/java/com/ghy/order/service/LogisticsService.java index 79724147..46051b82 100644 --- a/ghy-order/src/main/java/com/ghy/order/service/LogisticsService.java +++ b/ghy-order/src/main/java/com/ghy/order/service/LogisticsService.java @@ -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查询物流信息 * diff --git a/ghy-order/src/main/java/com/ghy/order/service/impl/LogisticsServiceImpl.java b/ghy-order/src/main/java/com/ghy/order/service/impl/LogisticsServiceImpl.java index 9ebc05f2..4fa6d528 100644 --- a/ghy-order/src/main/java/com/ghy/order/service/impl/LogisticsServiceImpl.java +++ b/ghy-order/src/main/java/com/ghy/order/service/impl/LogisticsServiceImpl.java @@ -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 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 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 headers = new HashMap<>(); + headers.put("Authorization", "APPCODE " + appCode); - // 计算数据签名 - 按照快递鸟官方文档 - // S1: RequestData(未编码) + ApiKey - String s1 = requestDataStr + appKey; - log.info("S1: {}", s1); + // 构建查询参数 + Map 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 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; } diff --git a/ghy-order/src/main/java/com/ghy/order/utils/HttpUtils.java b/ghy-order/src/main/java/com/ghy/order/utils/HttpUtils.java new file mode 100644 index 00000000..ab9201b5 --- /dev/null +++ b/ghy-order/src/main/java/com/ghy/order/utils/HttpUtils.java @@ -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 headers, + Map querys) throws Exception { + HttpClient httpClient = new DefaultHttpClient(); + HttpGet request = new HttpGet(buildUrl(host, path, querys)); + for (Map.Entry 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 headers, + Map querys, + Map bodys) throws Exception { + HttpClient httpClient = new DefaultHttpClient(); + HttpPost request = new HttpPost(buildUrl(host, path, querys)); + for (Map.Entry e : headers.entrySet()) { + request.addHeader(e.getKey(), e.getValue()); + } + + if (bodys != null) { + List nameValuePairList = new ArrayList(); + + 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 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 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(); + } +} \ No newline at end of file