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; } }