feat: add kuaidi100 logistics sync

This commit is contained in:
wushumin
2026-05-26 17:08:33 +08:00
parent 09d9fcbe69
commit a5f00d7e31
31 changed files with 2596 additions and 67 deletions

View File

@@ -0,0 +1,187 @@
<?php
namespace app\support;
class Kuaidi100Client
{
private const QUERY_URL = 'https://poll.kuaidi100.com/poll/query.do';
private const SUBSCRIBE_URL = 'https://poll.kuaidi100.com/poll';
private const AUTONUMBER_URL = 'http://www.kuaidi100.com/autonumber/auto';
private const COMPANY_CATALOG_URL = 'http://api.kuaidi100.com/manager/openapi/download/kdbm.do';
public function __construct(private ?Kuaidi100ConfigService $configService = null)
{
$this->configService ??= new Kuaidi100ConfigService();
}
public function query(string $companyCode, string $trackingNo, string $phone = ''): array
{
$config = $this->configService->getConfig();
if (!$this->configService->isReadyForQuery()) {
throw new \RuntimeException('快递100实时查询配置未完成');
}
$param = [
'com' => $companyCode,
'num' => $trackingNo,
'resultv2' => '1',
];
if ($phone !== '') {
$param['phone'] = $phone;
}
$paramJson = $this->encodeJson($param);
$response = $this->postForm(self::QUERY_URL, [
'customer' => $config['customer'],
'sign' => strtoupper(md5($paramJson . $config['key'] . $config['customer'])),
'param' => $paramJson,
]);
$decoded = json_decode($response, true);
if (!is_array($decoded)) {
throw new \RuntimeException('快递100实时查询返回格式异常');
}
return $decoded;
}
public function recognize(string $trackingNo): array
{
$trackingNo = trim($trackingNo);
if ($trackingNo === '') {
throw new \InvalidArgumentException('快递100单号不能为空');
}
if (!$this->configService->isReadyForRecognition()) {
throw new \RuntimeException('快递100智能识别配置未完成');
}
$config = $this->configService->getConfig();
$response = $this->requestGet(self::AUTONUMBER_URL, [
'num' => $trackingNo,
'key' => $config['key'],
]);
$decoded = json_decode($response, true);
if (!is_array($decoded)) {
throw new \RuntimeException('快递100智能识别返回格式异常');
}
return $decoded;
}
public function downloadCompanyCatalogWorkbook(): string
{
return $this->requestGet(self::COMPANY_CATALOG_URL, [], 30, 15);
}
public function subscribe(string $companyCode, string $trackingNo, string $phone = ''): array
{
$config = $this->configService->getConfig();
if (!$this->configService->isReadyForSubscribe()) {
throw new \RuntimeException('快递100订阅配置未完成');
}
$parameters = [
'callbackurl' => $config['callback_url'],
'resultv2' => '1',
];
if ($config['callback_salt'] !== '') {
$parameters['salt'] = $config['callback_salt'];
}
if ($phone !== '') {
$parameters['phone'] = $phone;
}
$requestPayload = [
'company' => $companyCode,
'number' => $trackingNo,
'key' => $config['key'],
'parameters' => $parameters,
];
if ($companyCode === '') {
unset($requestPayload['company']);
$requestPayload['autoCom'] = '1';
}
$paramJson = $this->encodeJson($requestPayload);
$response = $this->postForm(self::SUBSCRIBE_URL, [
'schema' => 'json',
'param' => $paramJson,
]);
$decoded = json_decode($response, true);
if (!is_array($decoded)) {
throw new \RuntimeException('快递100订阅返回格式异常');
}
return $decoded;
}
private function postForm(string $url, array $fields): string
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($fields),
CURLOPT_TIMEOUT => 8,
CURLOPT_CONNECTTIMEOUT => 4,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno) {
throw new \RuntimeException('快递100请求失败' . $error);
}
if ($httpStatus < 200 || $httpStatus >= 300) {
throw new \RuntimeException('快递100请求 HTTP 状态异常:' . $httpStatus);
}
return is_string($response) ? $response : '';
}
private function requestGet(string $url, array $query = [], int $timeout = 10, int $connectTimeout = 5): string
{
$fullUrl = $query ? $url . (str_contains($url, '?') ? '&' : '?') . http_build_query($query) : $url;
$ch = curl_init($fullUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPGET => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $connectTimeout,
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno) {
throw new \RuntimeException('快递100请求失败' . $error);
}
if ($httpStatus < 200 || $httpStatus >= 300) {
throw new \RuntimeException('快递100请求 HTTP 状态异常:' . $httpStatus);
}
return is_string($response) ? $response : '';
}
private function encodeJson(array $payload): string
{
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json)) {
throw new \RuntimeException('快递100请求参数编码失败');
}
return $json;
}
}