):array{http_status:int,body:array,request_id:string} */ private ?Closure $transport; public function __construct( ?string $baseUrl = null, ?string $appKey = null, ?string $appSecret = null, ?callable $transport = null ) { $this->baseUrl = rtrim($baseUrl ?? (string) Config::get('ej_pharmacy.base_url', ''), '/'); $this->appKey = $appKey ?? (string) Config::get('ej_pharmacy.app_key', ''); $this->appSecret = $appSecret ?? (string) Config::get('ej_pharmacy.app_secret', ''); if ($this->baseUrl === '' || $this->appKey === '' || $this->appSecret === '') { throw new RuntimeException('恩济药房接口未配置完整'); } $this->transport = $transport === null ? null : Closure::fromCallable($transport); } public static function isConfigured(): bool { return (bool) Config::get('ej_pharmacy.enabled', false) && trim((string) Config::get('ej_pharmacy.base_url', '')) !== '' && trim((string) Config::get('ej_pharmacy.app_key', '')) !== '' && trim((string) Config::get('ej_pharmacy.app_secret', '')) !== ''; } /** @return array{http_status:int,body:array,request_id:string} */ public function medicines(int $after = 0, int $limit = 100): array { return $this->request('GET', '/api/openapi/v1/medicines', null, [ 'after' => max($after, 0), 'limit' => min(max($limit, 1), 500), ]); } /** @param array $payload @return array{http_status:int,body:array,request_id:string} */ public function importMedicines(array $payload): array { return $this->request('POST', '/api/openapi/v1/medicine-imports', $payload); } /** @param array $payload @return array{http_status:int,body:array,request_id:string} */ public function createPrescriptionOrder(array $payload): array { return $this->request('POST', '/api/openapi/v1/prescription-orders', $payload); } /** @return array{http_status:int,body:array,request_id:string} */ public function prescriptionOrder(string $sourceOrderNo, int $sourceRevision = 0): array { $query = ['source_system' => 'zyt']; if ($sourceRevision > 0) { $query['source_revision'] = $sourceRevision; } return $this->request( 'GET', '/api/openapi/v1/prescription-orders/' . rawurlencode($sourceOrderNo), null, $query ); } /** @param array|null $payload @param array $query @return array{http_status:int,body:array,request_id:string} */ private function request(string $method, string $path, ?array $payload = null, array $query = []): array { $queryString = $query === [] ? '' : http_build_query($query, '', '&', PHP_QUERY_RFC3986); $pathWithQuery = $path . ($queryString !== '' ? '?' . $queryString : ''); $body = $payload === null ? '' : (string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); $timestamp = (string) time(); $nonce = bin2hex(random_bytes(16)); $requestId = bin2hex(random_bytes(16)); $canonical = EjPharmacySignature::canonical($method, $pathWithQuery, $timestamp, $nonce, $body); $headers = [ 'Accept: application/json', 'Content-Type: application/json; charset=utf-8', 'X-App-Key: ' . $this->appKey, 'X-Timestamp: ' . $timestamp, 'X-Nonce: ' . $nonce, 'X-Signature: ' . EjPharmacySignature::sign($this->appSecret, $canonical), 'X-Request-Id: ' . $requestId, 'Expect:', ]; if ($this->transport !== null) { return ($this->transport)(strtoupper($method), $pathWithQuery, $body, $headers); } $configuredTransport = strtolower((string) Config::get('ej_pharmacy.http_transport', 'auto')); $curlSsl = strtoupper((string) ((function_exists('curl_version') ? curl_version() : [])['ssl_version'] ?? '')); if ($configuredTransport === 'openssl' || ($configuredTransport === 'auto' && str_starts_with($curlSsl, 'NSS/')) ) { return $this->requestWithOpenSsl($method, $pathWithQuery, $body, $headers, $requestId); } $ch = curl_init($this->baseUrl . $pathWithQuery); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => strtoupper($method), CURLOPT_HTTPHEADER => $headers, CURLOPT_CONNECTTIMEOUT => (int) Config::get('ej_pharmacy.connect_timeout', 5), CURLOPT_TIMEOUT => (int) Config::get('ej_pharmacy.request_timeout', 30), CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); if ($payload !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $raw = curl_exec($ch); $httpStatus = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($raw === false) { throw new RuntimeException('恩济药房通信失败:' . $error); } $decoded = json_decode((string) $raw, true); if (!is_array($decoded)) { throw new RuntimeException('恩济药房返回了无效 JSON,HTTP ' . $httpStatus); } return ['http_status' => $httpStatus, 'body' => $decoded, 'request_id' => $requestId]; } /** @param array $headers @return array{http_status:int,body:array,request_id:string} */ private function requestWithOpenSsl( string $method, string $path, string $body, array $headers, string $requestId ): array { $ssl = [ 'verify_peer' => true, 'verify_peer_name' => true, 'allow_self_signed' => false, ]; $caFile = trim((string) Config::get('ej_pharmacy.ca_file', '')); if ($caFile !== '') { $ssl['cafile'] = $caFile; } $context = stream_context_create([ 'http' => [ 'method' => strtoupper($method), 'header' => implode("\r\n", $headers), 'content' => $body, 'ignore_errors' => true, 'timeout' => (int) Config::get('ej_pharmacy.request_timeout', 30), 'protocol_version' => 1.1, ], 'ssl' => $ssl, ]); $raw = @file_get_contents($this->baseUrl . $path, false, $context); if ($raw === false) { $lastError = error_get_last(); $message = is_array($lastError) ? (string) ($lastError['message'] ?? '') : ''; throw new RuntimeException('恩济药房通信失败:' . ($message !== '' ? $message : 'OpenSSL 请求失败')); } $httpStatus = 0; $responseHeaders = $http_response_header ?? []; foreach (array_reverse($responseHeaders) as $responseHeader) { if (preg_match('/^HTTP\/\S+\s+(\d{3})\b/i', $responseHeader, $matches)) { $httpStatus = (int) $matches[1]; break; } } $decoded = json_decode((string) $raw, true); if (!is_array($decoded)) { throw new RuntimeException('恩济药房返回了无效 JSON,HTTP ' . $httpStatus); } return ['http_status' => $httpStatus, 'body' => $decoded, 'request_id' => $requestId]; } }