360 行
12 KiB
PHP
360 行
12 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro_aliyun_express\library\provider;
|
||
|
||
use addons\shopro_aliyun_express\library\ExpressConfig;
|
||
|
||
class AliyunExpressProvider implements ProviderInterface
|
||
{
|
||
protected $config;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->config = ExpressConfig::getInstance();
|
||
}
|
||
|
||
public function getName()
|
||
{
|
||
return 'aliyun';
|
||
}
|
||
|
||
public function query($waybillNo, $phone = '', $expressCode = '')
|
||
{
|
||
$appcode = $this->config->getAliyunAppcode();
|
||
if ($appcode === '') {
|
||
throw new \Exception('请配置阿里云 APPCODE');
|
||
}
|
||
|
||
$apiUrl = $this->config->getAliyunApiUrl();
|
||
if ($apiUrl === '') {
|
||
throw new \Exception('请填写「接口地址」(从云市场该商品 API 调试页复制完整 URL)');
|
||
}
|
||
|
||
$phoneParam = $this->resolvePhone($waybillNo, $phone);
|
||
$attempts = $this->buildAttempts($waybillNo, $phone, $phoneParam, $expressCode);
|
||
|
||
$errors = [];
|
||
foreach ($attempts as $attempt) {
|
||
try {
|
||
$http = $this->sendHttp($appcode, $attempt);
|
||
$parsed = $this->parseResponseBody($http['body']);
|
||
if (!$this->isBusinessSuccess($parsed)) {
|
||
throw new \Exception($this->extractBusinessError($parsed));
|
||
}
|
||
|
||
return [
|
||
'provider' => $this->getName(),
|
||
'success' => true,
|
||
'raw' => $parsed,
|
||
'data' => $parsed,
|
||
];
|
||
} catch (\Exception $e) {
|
||
$label = ($attempt['label'] ?? 'request') . ' [' . ($attempt['method'] ?? '') . ' ' . ($attempt['url'] ?? '') . ']';
|
||
$errors[] = $label . ':' . $e->getMessage();
|
||
}
|
||
}
|
||
|
||
$message = implode(';', $errors);
|
||
if ($message === '') {
|
||
$message = '阿里云快递查询失败';
|
||
}
|
||
throw new \Exception($message);
|
||
}
|
||
|
||
protected function buildAttempts($waybillNo, $phone, $phoneParam, $expressCode)
|
||
{
|
||
$mode = $this->config->getAliyunApiMode();
|
||
$apiUrl = $this->config->getAliyunApiUrl();
|
||
$httpMethod = strtolower($this->config->getAliyunHttpMethod());
|
||
|
||
if ($mode === 'auto' && $this->isKzexpressUrl($apiUrl)) {
|
||
$mode = 'kzexpress';
|
||
}
|
||
|
||
if ($mode === 'kzexpress') {
|
||
$params = $this->kzexpressParams($waybillNo, $phone, $phoneParam, $expressCode);
|
||
$attempts = [
|
||
['label' => 'kzexpress_get', 'method' => 'GET', 'url' => $apiUrl, 'query' => $params],
|
||
['label' => 'kzexpress_post', 'method' => 'POST', 'url' => $apiUrl, 'body' => $params],
|
||
];
|
||
if ($httpMethod === 'get') {
|
||
return [ $attempts[0] ];
|
||
}
|
||
if ($httpMethod === 'post') {
|
||
return [ $attempts[1] ];
|
||
}
|
||
return $attempts;
|
||
}
|
||
|
||
if ($mode === 'wuliu_kdi') {
|
||
$url = $apiUrl !== '' && stripos($apiUrl, 'wuliu') !== false
|
||
? $apiUrl
|
||
: 'https://wuliu.market.alicloudapi.com/kdi';
|
||
return [[
|
||
'label' => 'wuliu_kdi',
|
||
'method' => 'GET',
|
||
'url' => $url,
|
||
'query' => array_filter([
|
||
'no' => $waybillNo,
|
||
'type' => $expressCode !== '' ? strtolower($expressCode) : 'auto',
|
||
]),
|
||
]];
|
||
}
|
||
|
||
if ($mode === 'expr_get') {
|
||
return [[
|
||
'label' => 'expr_get',
|
||
'method' => 'GET',
|
||
'url' => $apiUrl,
|
||
'query' => $this->exprParams($waybillNo, $phoneParam),
|
||
]];
|
||
}
|
||
|
||
if ($mode === 'expr_post') {
|
||
return [[
|
||
'label' => 'expr_post',
|
||
'method' => 'POST',
|
||
'url' => $apiUrl,
|
||
'body' => $this->exprParams($waybillNo, $phoneParam),
|
||
]];
|
||
}
|
||
|
||
// auto:按配置优先,404/格式错误时自动换 GET/POST
|
||
$attempts = [];
|
||
if ($httpMethod === 'get') {
|
||
$attempts[] = ['label' => 'expr_get', 'method' => 'GET', 'url' => $apiUrl, 'query' => $this->exprParams($waybillNo, $phoneParam)];
|
||
} elseif ($httpMethod === 'post') {
|
||
$attempts[] = ['label' => 'expr_post', 'method' => 'POST', 'url' => $apiUrl, 'body' => $this->exprParams($waybillNo, $phoneParam)];
|
||
} else {
|
||
$attempts[] = ['label' => 'expr_get', 'method' => 'GET', 'url' => $apiUrl, 'query' => $this->exprParams($waybillNo, $phoneParam)];
|
||
$attempts[] = ['label' => 'expr_post', 'method' => 'POST', 'url' => $apiUrl, 'body' => $this->exprParams($waybillNo, $phoneParam)];
|
||
}
|
||
|
||
return $attempts;
|
||
}
|
||
|
||
protected function isKzexpressUrl($url)
|
||
{
|
||
return stripos($url, 'kzexpress.market.alicloudapi.com') !== false
|
||
|| stripos($url, '/api-mall/api/express/query') !== false;
|
||
}
|
||
|
||
protected function kzexpressParams($waybillNo, $phone, $phoneParam, $expressCode)
|
||
{
|
||
$mobile = trim((string) $phone);
|
||
if ($mobile === '' && $phoneParam !== '') {
|
||
$mobile = $phoneParam;
|
||
} elseif (strlen($mobile) > 4) {
|
||
// 顺丰/京东等常用后四位;若接口需要完整手机号可改为不截取
|
||
$mobile = substr($mobile, -4);
|
||
}
|
||
|
||
$params = [
|
||
'expressNo' => $waybillNo,
|
||
];
|
||
if ($mobile !== '') {
|
||
$params['mobile'] = $mobile;
|
||
}
|
||
if ($expressCode !== '') {
|
||
$params['cpCode'] = $expressCode;
|
||
}
|
||
|
||
return $params;
|
||
}
|
||
|
||
protected function exprParams($waybillNo, $phoneParam)
|
||
{
|
||
return array_filter([
|
||
'waybillNumber' => $waybillNo,
|
||
'phoneNumber' => $phoneParam,
|
||
'phone' => $phoneParam,
|
||
'nu' => $waybillNo,
|
||
'com' => 'auto',
|
||
], function ($v) {
|
||
return $v !== '' && $v !== null;
|
||
});
|
||
}
|
||
|
||
protected function sendHttp($appcode, array $attempt)
|
||
{
|
||
$method = strtoupper($attempt['method'] ?? 'GET');
|
||
$url = trim($attempt['url'] ?? '');
|
||
if ($url === '') {
|
||
throw new \Exception('接口地址为空');
|
||
}
|
||
|
||
$headers = [
|
||
'Authorization: APPCODE ' . $appcode,
|
||
'Accept: application/json',
|
||
];
|
||
|
||
if ($method === 'GET') {
|
||
$query = $attempt['query'] ?? [];
|
||
if (!empty($query)) {
|
||
$url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($query);
|
||
}
|
||
$postFields = null;
|
||
} else {
|
||
$body = $attempt['body'] ?? [];
|
||
$postFields = is_array($body) ? http_build_query($body) : (string) $body;
|
||
$headers[] = 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8';
|
||
}
|
||
|
||
$ch = curl_init();
|
||
$opts = [
|
||
CURLOPT_URL => $url,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_CONNECTTIMEOUT => 10,
|
||
CURLOPT_TIMEOUT => 15,
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_SSL_VERIFYHOST => false,
|
||
CURLOPT_HTTPHEADER => $headers,
|
||
];
|
||
if ($method === 'POST') {
|
||
$opts[CURLOPT_POST] = true;
|
||
$opts[CURLOPT_POSTFIELDS] = $postFields;
|
||
} else {
|
||
$opts[CURLOPT_CUSTOMREQUEST] = 'GET';
|
||
}
|
||
curl_setopt_array($ch, $opts);
|
||
|
||
$response = curl_exec($ch);
|
||
$errno = curl_errno($ch);
|
||
$error = curl_error($ch);
|
||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($response === false || $errno) {
|
||
throw new \Exception('网络错误 ' . ($error ?: ('errno ' . $errno)));
|
||
}
|
||
|
||
if ($httpCode === 404) {
|
||
throw new \Exception('HTTP 404 接口不存在,请核对云市场控制台中的完整 URL');
|
||
}
|
||
if ($httpCode >= 400) {
|
||
$detail = $this->previewBody($response);
|
||
if ($detail === '' && $response !== '') {
|
||
$detail = $this->previewBody($response);
|
||
}
|
||
$jsonHint = json_decode($response, true);
|
||
if (is_array($jsonHint)) {
|
||
foreach (['msg', 'message', 'errorMsg', 'errMsg'] as $key) {
|
||
if (!empty($jsonHint[$key])) {
|
||
$detail = (string) $jsonHint[$key];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
throw new \Exception('HTTP ' . $httpCode . ($detail !== '' ? ':' . $detail : '(请检查 expressNo/mobile 参数及 APPCODE)'));
|
||
}
|
||
|
||
return ['body' => $response, 'http_code' => $httpCode];
|
||
}
|
||
|
||
protected function parseResponseBody($body)
|
||
{
|
||
$body = trim((string) $body);
|
||
if ($body === '') {
|
||
throw new \Exception('返回空响应');
|
||
}
|
||
if (substr($body, 0, 3) === "\xEF\xBB\xBF") {
|
||
$body = substr($body, 3);
|
||
}
|
||
$parsed = json_decode($body, true);
|
||
if (!is_array($parsed)) {
|
||
throw new \Exception('非 JSON:' . $this->previewBody($body));
|
||
}
|
||
return $parsed;
|
||
}
|
||
|
||
protected function isBusinessSuccess(array $parsed)
|
||
{
|
||
if (array_key_exists('success', $parsed) && ($parsed['success'] === false || $parsed['success'] === 'false' || $parsed['success'] === 0)) {
|
||
return false;
|
||
}
|
||
|
||
if (isset($parsed['code']) && in_array((string) $parsed['code'], ['200', '0'], true)) {
|
||
if (!empty($parsed['data']['logisticsTraceDetailList'])
|
||
|| !empty($parsed['data']['logisticsTraceDetails'])
|
||
|| !empty($parsed['logisticsTraceDetails'])) {
|
||
return true;
|
||
}
|
||
if (!empty($parsed['data']['logisticsEvents']) || !empty($parsed['result']['list'])) {
|
||
return true;
|
||
}
|
||
if (isset($parsed['data']) && is_array($parsed['data']) && !empty($parsed['data']['expressNo'])) {
|
||
return true;
|
||
}
|
||
if (!isset($parsed['data']) || !array_key_exists('success', $parsed)) {
|
||
return true;
|
||
}
|
||
return !empty($parsed['success']);
|
||
}
|
||
if (isset($parsed['status']) && (string) $parsed['status'] === '200') {
|
||
return true;
|
||
}
|
||
if (isset($parsed['status']) && (string) $parsed['status'] === '0') {
|
||
return true;
|
||
}
|
||
if (isset($parsed['ret_code']) && intval($parsed['ret_code']) === 0) {
|
||
return true;
|
||
}
|
||
if (!empty($parsed['Success'])) {
|
||
return true;
|
||
}
|
||
if (!empty($parsed['success']) && $parsed['success'] !== false) {
|
||
return true;
|
||
}
|
||
if (!empty($parsed['data']['logisticsEvents']) || !empty($parsed['result']['list'])) {
|
||
return true;
|
||
}
|
||
if (!empty($parsed['data']['logisticsTraceDetailList'])
|
||
|| !empty($parsed['data']['logisticsTraceDetails'])
|
||
|| !empty($parsed['logisticsTraceDetails'])) {
|
||
return true;
|
||
}
|
||
if (isset($parsed['data']) && is_array($parsed['data']) && !empty($parsed['data']['mailNo'])) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
protected function extractBusinessError(array $parsed)
|
||
{
|
||
foreach (['msg', 'message', 'Reason', 'error_msg', 'errMsg', 'errorMsg'] as $key) {
|
||
if (!empty($parsed[$key])) {
|
||
return (string) $parsed[$key];
|
||
}
|
||
}
|
||
if (!empty($parsed['data']['message'])) {
|
||
return (string) $parsed['data']['message'];
|
||
}
|
||
if (isset($parsed['code']) && !in_array((string) $parsed['code'], ['200', '0'], true)) {
|
||
return '接口返回 code=' . $parsed['code'];
|
||
}
|
||
return '业务返回失败';
|
||
}
|
||
|
||
protected function previewBody($body)
|
||
{
|
||
$text = trim(strip_tags((string) $body));
|
||
$text = preg_replace('/\s+/', ' ', $text);
|
||
if (function_exists('mb_substr')) {
|
||
return mb_substr($text, 0, 200);
|
||
}
|
||
return substr($text, 0, 200);
|
||
}
|
||
|
||
protected function resolvePhone($waybillNo, $phone)
|
||
{
|
||
$phone = trim((string) $phone);
|
||
if ($phone === '') {
|
||
return '';
|
||
}
|
||
if (strlen($phone) >= 4) {
|
||
return substr($phone, -4);
|
||
}
|
||
return $phone;
|
||
}
|
||
}
|