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