first commit

This commit is contained in:
Your Name
2026-09-08 11:40:15 +08:00
commit a5353f7eb5
9568 changed files with 1646214 additions and 0 deletions
@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace app\common\service\pharmacy;
use Closure;
use RuntimeException;
use think\facade\Config;
final class EjPharmacyClient
{
private string $baseUrl;
private string $appKey;
private string $appSecret;
/** @var null|Closure(string,string,string,array<int,string>):array{http_status:int,body:array<string,mixed>,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<string,mixed>,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<string,mixed> $payload @return array{http_status:int,body:array<string,mixed>,request_id:string} */
public function importMedicines(array $payload): array
{
return $this->request('POST', '/api/openapi/v1/medicine-imports', $payload);
}
/** @param array<string,mixed> $payload @return array{http_status:int,body:array<string,mixed>,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<string,mixed>,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<string,mixed>|null $payload @param array<string,int|string> $query @return array{http_status:int,body:array<string,mixed>,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('恩济药房返回了无效 JSONHTTP ' . $httpStatus);
}
return ['http_status' => $httpStatus, 'body' => $decoded, 'request_id' => $requestId];
}
/** @param array<int,string> $headers @return array{http_status:int,body:array<string,mixed>,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('恩济药房返回了无效 JSONHTTP ' . $httpStatus);
}
return ['http_status' => $httpStatus, 'body' => $decoded, 'request_id' => $requestId];
}
}