products(1, 100); * $order = $api->createOrder('PRODUCT_ID', 1, 'my_site_order_123'); * $delivery = $api->orderDelivery($order['order_code']); * // Với đơn loại 1, xem $delivery['delivery_codes_detail'] để biết code đã sử dụng hay chưa. * // Nếu là đơn file, dùng $api->downloadOrderFile($order['order_code'], __DIR__ . '/order.zip'); * * Lưu ý: * - Không để API Key trong JavaScript frontend. * - Khi gặp HTTP 429/rate_limited, hãy chờ rồi retry với cùng request_id. * - Nếu admin bật HMAC, gọi $api->enableHmac(true). */ class ShopApiClient { private $baseUrl; private $apiKey; private $useHmac = false; private $connectTimeout = 5; private $timeout = 15; public function __construct($baseUrl, $apiKey) { $this->baseUrl = rtrim($baseUrl, '/'); $this->apiKey = $apiKey; } public function enableHmac($enabled = true) { $this->useHmac = (bool)$enabled; return $this; } public function setTimeouts($connectTimeout, $timeout) { $this->connectTimeout = max(2, (int)$connectTimeout); $this->timeout = max($this->connectTimeout, (int)$timeout); return $this; } private function request($method, $path, array $body = null) { $method = strtoupper($method); $url = $this->baseUrl . '/' . ltrim($path, '/'); $headers = array( 'Accept: application/json', 'Authorization: Bearer ' . $this->apiKey, ); $payload = ''; if ($body !== null) { $payload = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $headers[] = 'Content-Type: application/json'; } if ($this->useHmac && $method === 'POST') { $timestamp = (string)time(); $nonce = bin2hex(random_bytes(12)); $pathOnly = parse_url($url, PHP_URL_PATH); $base = $method . "\n" . $pathOnly . "\n" . $timestamp . "\n" . $nonce . "\n" . $payload; $headers[] = 'X-Api-Timestamp: ' . $timestamp; $headers[] = 'X-Api-Nonce: ' . $nonce; $headers[] = 'X-Api-Signature: sha256=' . hash_hmac('sha256', $base, $this->apiKey); } $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connectTimeout); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_NOSIGNAL, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); if ($payload !== '') curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); $raw = curl_exec($ch); $error = curl_error($ch); $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($raw === false) throw new Exception('Không gọi được API: ' . $error); $json = json_decode($raw, true); if (!is_array($json)) throw new Exception('API không trả JSON hợp lệ. HTTP ' . $status); if ($status === 429 || (isset($json['error']) && $json['error'] === 'rate_limited')) { throw new Exception('API đang giới hạn tốc độ, hãy chờ rồi retry với cùng request_id.'); } if (empty($json['ok'])) { $message = isset($json['message']) ? $json['message'] : ('API lỗi HTTP ' . $status); throw new Exception($message); } return isset($json['data']) ? $json['data'] : $json; } public function categories() { return $this->request('GET', '/api/v1/categories'); } public function products($page = 1, $limit = 100) { return $this->request('GET', '/api/v1/products?page=' . (int)$page . '&limit=' . (int)$limit); } public function product($productId) { return $this->request('GET', '/api/v1/product?id=' . rawurlencode($productId)); } public function balance() { return $this->request('GET', '/api/v1/balance'); } public function createOrder($productId, $quantity, $requestId = '') { if ($requestId === '') $requestId = 'req_' . date('YmdHis') . '_' . bin2hex(random_bytes(4)); return $this->request('POST', '/api/v1/order/create', array( 'product_id' => $productId, 'quantity' => (int)$quantity, 'request_id' => $requestId, )); } public function orderStatus($orderCode) { return $this->request('GET', '/api/v1/order/status?order_code=' . rawurlencode($orderCode)); } public function orderDelivery($orderCode) { // Với sản phẩm loại 1/text, response có delivery_codes_detail gồm: // code, used, used_at, status_label. return $this->request('GET', '/api/v1/order/delivery?order_code=' . rawurlencode($orderCode)); } public function downloadOrderFile($orderCode, $saveTo) { $url = $this->baseUrl . '/api/v1/order-download?order_code=' . rawurlencode($orderCode); $fp = fopen($saveTo, 'wb'); if (!$fp) throw new Exception('Không mở được file lưu: ' . $saveTo); $ch = curl_init($url); curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $this->apiKey)); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connectTimeout); curl_setopt($ch, CURLOPT_TIMEOUT, max(60, $this->timeout * 4)); curl_setopt($ch, CURLOPT_NOSIGNAL, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); $ok = curl_exec($ch); $error = curl_error($ch); $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); fclose($fp); if (!$ok || $status >= 400) { @unlink($saveTo); throw new Exception('Tải file thất bại. HTTP ' . $status . ($error ? ': ' . $error : '')); } return $saveTo; } } // Ví dụ: // $api = new ShopApiClient('https://domain.com', 'YOUR_API_KEY'); // $api->enableHmac(false); // đổi thành true nếu admin yêu cầu HMAC // $products = $api->products(1, 100); // print_r($products);