baseUrl = rtrim($baseUrl, '/'); $this->appCode = $appCode; $this->appSecret = $appSecret; $this->debug = $debug; } /** * 发送 GET 请求 * * @param string $path 接口路径(如:/open/api/student/list) * @param array $params 请求参数 * @return array 响应数据 * @throws Exception */ public function get(string $path, array $params = []): array { return $this->request('GET', $path, $params); } /** * 发送 POST 请求 * * @param string $path 接口路径 * @param array $params 请求参数 * @return array 响应数据 * @throws Exception */ public function post(string $path, array $params = []): array { return $this->request('POST', $path, $params); } /** * 发送请求 * * @param string $method 请求方法(GET/POST) * @param string $path 接口路径 * @param array $params 请求参数 * @return array 响应数据 * @throws Exception */ private function request(string $method, string $path, array $params): array { // 1. 计算签名 $timestamp = $this->getTimestamp(); $sign = $this->calculateSign($params); // 2. 构建请求头 $headers = [ 'X-App-Id: ' . $this->appCode, 'X-Timestamp: ' . $timestamp, 'X-Sign: ' . $sign, 'Content-Type: application/json' ]; // 3. 构建完整 URL $url = $this->baseUrl . $path; if ($method === 'GET' && !empty($params)) { $url .= '?' . http_build_query($params); } // 4. 发送请求 if ($this->debug) { $this->printDebugInfo($method, $url, $headers, $params, $sign); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_TIMEOUT, 30); if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params)); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error) { throw new Exception("请求失败: {$error}"); } if ($this->debug) { echo "\n【响应状态码】{$httpCode}\n"; echo "【响应内容】\n{$response}\n\n"; } // 5. 解析响应 $data = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception("响应解析失败: " . json_last_error_msg()); } return $data; } /** * 计算签名 * * 签名算法: * 1. 将所有请求参数按 key 的 ASCII 码升序排序 * 2. 按照 key1=value1&key2=value2... 格式拼接 * 3. 末尾追加 &appSecret=xxx * 4. 对整个字符串进行 MD5 加密,转大写 * * @param array $params 请求参数 * @return string 签名字符串 */ private function calculateSign(array $params): string { // 1. 按 key 升序排序 ksort($params); // 2. 拼接参数 $signStr = ''; foreach ($params as $key => $value) { // 跳过空值 if ($value === '' || $value === null) { continue; } if ($signStr !== '') { $signStr .= '&'; } $signStr .= $key . '=' . $value; } // 3. 追加密钥 if ($signStr !== '') { $signStr .= '&'; } $signStr .= 'appSecret=' . $this->appSecret; // 4. MD5 加密并转大写 return strtoupper(md5($signStr)); } /** * 获取当前时间戳(毫秒) * * @return int 时间戳 */ private function getTimestamp(): int { return intval(microtime(true) * 1000); } /** * 打印调试信息 */ private function printDebugInfo(string $method, string $url, array $headers, array $params, string $sign): void { echo "========================================\n"; echo "【开放接口调用】\n"; echo "========================================\n"; echo "【请求方法】{$method}\n"; echo "【请求 URL】{$url}\n"; echo "【请求头】\n"; foreach ($headers as $header) { echo " {$header}\n"; } echo "【请求参数】\n"; echo json_encode($params, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; // 显示签名计算过程 ksort($params); $signStr = ''; foreach ($params as $key => $value) { if ($value === '' || $value === null) continue; if ($signStr !== '') $signStr .= '&'; $signStr .= $key . '=' . $value; } if ($signStr !== '') $signStr .= '&'; $signStr .= 'appSecret=' . $this->appSecret; echo "【签名计算】\n"; echo " 签名字符串: {$signStr}\n"; echo " MD5 结果: {$sign}\n"; echo "========================================\n"; } /** * 静态工厂方法 */ public static function create(string $baseUrl, string $appCode, string $appSecret, bool $debug = false): self { return new self($baseUrl, $appCode, $appSecret, $debug); } }