文件
xionggeshop/addons/shopro_aliyun_express/library/provider/AliyunExpressProvider.php

360 行
12 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

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