config = $config; $this->encryption = new CcbEncryption($config); } /** * 发送请求到建行 * * @param string $txCode 交易代码 (如: A3341TP01) * @param array $bodyData 业务数据 * @return array 解密后的响应数据 * @throws Exception */ public function request($txCode, $bodyData) { // 记录开始时间 $startTime = microtime(true); try { // 1. 获取接口地址 $url = $this->getApiUrl($txCode); // 2. 构建加密报文 $requestData = $this->encryption->buildEncryptedMessage($txCode, $bodyData); // 3. 记录请求日志 $this->logRequest($txCode, $bodyData, $requestData); // 4. 发送HTTP请求 (带重试机制) $response = $this->retry(function () use ($url, $requestData) { return $this->post($url, $requestData); }); // 5. 解析响应 $result = $this->encryption->parseResponse($response); // 6. 记录响应日志 $costTime = round((microtime(true) - $startTime) * 1000, 2); $this->logResponse($txCode, $result, $costTime); // 7. 检查业务返回码 $this->checkReturnCode($result); return $result; } catch (Exception $e) { // 记录错误日志 $costTime = round((microtime(true) - $startTime) * 1000, 2); $this->logError($txCode, $e->getMessage(), $costTime); throw $e; } } /** * 发送POST请求 * * @param string $url 请求URL * @param array $data 请求数据 * @return string 响应内容 * @throws Exception */ private function post($url, $data) { // 初始化CURL $ch = curl_init(); // 设置CURL选项 curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, $this->config['http']['timeout'] ?? 30); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Accept: application/json', 'Content-Type: application/json', ]); // 如果是HTTPS,验证证书 if (strpos($url, 'https') === 0) { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); } // 执行请求 $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); // 检查CURL错误 if ($error) { throw new Exception('CURL错误: ' . $error); } // 检查HTTP状态码 if ($httpCode == 404) { throw new Exception('接口404,请检查请求头是否包含Accept和Content-Type'); } if ($httpCode !== 200) { throw new Exception('HTTP错误码: ' . $httpCode . ', 响应: ' . $response); } return $response; } /** * 重试机制 * * @param callable $callable 要执行的函数 * @param int|null $maxRetries 最大重试次数 * @return mixed 执行结果 * @throws Exception */ private function retry($callable, $maxRetries = null) { if ($maxRetries === null) { $maxRetries = $this->config['http']['retry_times'] ?? 3; } $delays = $this->config['http']['retry_delay'] ?? [1, 2, 5]; $lastException = null; for ($i = 0; $i <= $maxRetries; $i++) { try { return $callable(); } catch (Exception $e) { $lastException = $e; // 如果是最后一次尝试,不再重试 if ($i >= $maxRetries) { break; } // 等待后重试 $delay = $delays[$i] ?? 5; $this->logRetry($i + 1, $maxRetries, $delay, $e->getMessage()); sleep($delay); } } throw new Exception('请求失败,已重试' . $maxRetries . '次: ' . $lastException->getMessage()); } /** * 获取API地址 * * @param string $txCode 交易代码 * @return string 完整API地址 */ private function getApiUrl($txCode) { $baseUrl = $this->config['api_base_url'] ?? ''; if (empty($baseUrl)) { throw new Exception('API基础地址未配置'); } return $baseUrl . '?txcode=' . $txCode; } /** * 检查业务返回码 * * @param array $result 响应数据 * @throws Exception */ private function checkReturnCode($result) { // 检查CLD_HEADER中的RET_CODE $retCode = $result['CLD_HEADER']['RET_CODE'] ?? ''; $retMsg = $result['CLD_HEADER']['RET_MSG'] ?? '未知错误'; if ($retCode !== '000000') { throw new Exception('建行接口返回错误[' . $retCode . ']: ' . $retMsg); } } /** * 记录请求日志 * * @param string $txCode 交易代码 * @param array $bodyData 业务数据 * @param array $requestData 加密后的请求数据 */ private function logRequest($txCode, $bodyData, $requestData) { if (!($this->config['log']['enabled'] ?? true)) { return; } Log::info('[建行请求] ' . $txCode . ' svcid:' . $requestData['svcid'] . ' mac:' . $requestData['mac'] . ' cnt_length:' . strlen($requestData['cnt']) . ' body_data:' . json_encode($bodyData, JSON_UNESCAPED_UNICODE)); } /** * 记录响应日志 * * @param string $txCode 交易代码 * @param array $result 响应数据 * @param float $costTime 耗时(毫秒) */ private function logResponse($txCode, $result, $costTime) { if (!($this->config['log']['enabled'] ?? true)) { return; } Log::info('[建行响应] ' . $txCode . ' ret_code:' . ($result['CLD_HEADER']['RET_CODE'] ?? '') . ' ret_msg:' . ($result['CLD_HEADER']['RET_MSG'] ?? '') . ' cost_time:' . $costTime . 'ms'); } /** * 记录错误日志 * * @param string $txCode 交易代码 * @param string $errorMsg 错误信息 * @param float $costTime 耗时(毫秒) */ private function logError($txCode, $errorMsg, $costTime) { if (!($this->config['log']['enabled'] ?? true)) { return; } Log::error('[建行错误] ' . $txCode . ' error:' . $errorMsg . ' cost_time:' . $costTime . 'ms'); } /** * 记录重试日志 * * @param int $currentRetry 当前重试次数 * @param int $maxRetries 最大重试次数 * @param int $delay 延迟秒数 * @param string $reason 重试原因 */ private function logRetry($currentRetry, $maxRetries, $delay, $reason) { if (!($this->config['log']['enabled'] ?? true)) { return; } Log::warning('[建行重试] retry:' . $currentRetry . '/' . $maxRetries . ' delay:' . $delay . 's reason:' . $reason); } }