config = $config; $this->validateConfig(); } /** * 发送API请求 * * @param string $txCode 交易码(如svc_occMebOrderPush) * @param array $body 请求体数据 * @param string $txSeq 交易流水号,不传则自动生成 * @return array 响应数据 * @throws \Exception */ public function request($txCode, $body, $txSeq = null) { // 生成交易流水号 if (empty($txSeq)) { $txSeq = CcbMD5::generateTransactionSeq(); } // 构建请求报文 $message = $this->buildMessage($txCode, $body, $txSeq); // 📝 记录原始请求报文(加密前) Log::info('建行生活API原始请求报文 [txCode=' . $txCode . '] [txSeq=' . $txSeq . ']'); Log::info('原始报文内容: ' . $message); // 使用服务方公钥加密报文 $encryptedMessage = CcbRSA::encryptForCcb($message, $this->config['public_key']); // 移除BASE64中的换行符 $encryptedMessage = str_replace(["\r", "\n", "\r\n"], '', $encryptedMessage); // 使用服务方私钥签名 $mac = CcbMD5::signApiMessage($message, $this->config['private_key']); // 发送HTTP请求 $response = $this->sendHttpRequest($txCode, $encryptedMessage, $mac); // 处理响应 return $this->handleResponse($response); } /** * 构建请求报文 * * @param string $txCode 交易码 * @param array $body 请求体 * @param string $txSeq 交易流水号 * @return string JSON格式的报文 */ private function buildMessage($txCode, $body, $txSeq) { $message = [ 'CLD_HEADER' => [ 'CLD_TX_CHNL' => $this->config['service_id'], 'CLD_TX_TIME' => date('YmdHis'), 'CLD_TX_CODE' => $txCode, 'CLD_TX_SEQ' => $txSeq ], 'CLD_BODY' => $body ]; // 转换为JSON(不转义中文) return json_encode($message, JSON_UNESCAPED_UNICODE); } /** * 发送HTTP请求 * * @param string $txCode 交易代码(用于构建URL) * @param string $cnt 加密后的报文内容 * @param string $mac 签名 * @return string 响应内容 * @throws \Exception */ private function sendHttpRequest($txCode, $cnt, $mac) { // 记录请求开始时间 $startTime = microtime(true); // 构建完整的API URL(基础URL + ?txcode=交易代码) $apiUrl = $this->config['api_base_url'] . '?txcode=' . $txCode; // 构建请求参数(按照建行报文规范:cnt、mac、svcid) $params = [ 'cnt' => $cnt, 'mac' => $mac, 'svcid' => $this->config['service_id'] ]; // ✅ 将请求参数转为JSON格式(按照建行报文示例要求) $jsonParams = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); // 📝 记录请求参数(加密后的完整内容) Log::info('建行生活API加密请求 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [timeout=' . self::DEFAULT_TIMEOUT . 's]'); Log::info('JSON请求体: ' . $jsonParams); // 初始化CURL $ch = curl_init(); // 设置CURL选项(按照建行要求发送JSON格式) curl_setopt_array($ch, [ CURLOPT_URL => $apiUrl, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $jsonParams, // ✅ 发送JSON格式,不是URL编码 CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => self::DEFAULT_TIMEOUT, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json; charset=utf-8', // ✅ JSON格式 'Accept: application/json' ] ]); // 执行请求 $response = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlInfo = curl_getinfo($ch); curl_close($ch); // 计算请求耗时 $costTime = round((microtime(true) - $startTime) * 1000, 2); // 毫秒 // 检查错误 if ($error) { // 📝 记录错误日志 Log::error('建行生活API请求失败 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [error=' . $error . '] [cost_time=' . $costTime . 'ms] [total_time=' . ($curlInfo['total_time'] ?? 0) . 's] [connect_time=' . ($curlInfo['connect_time'] ?? 0) . 's]'); throw new \Exception('HTTP请求失败: ' . $error); } if ($httpCode !== 200) { // 📝 记录HTTP状态码异常日志 $responsePreview = mb_substr($response, 0, 300); Log::error('建行生活API响应状态码异常 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [http_code=' . $httpCode . '] [cost_time=' . $costTime . 'ms] [response=' . $responsePreview . ']'); throw new \Exception('HTTP状态码异常: ' . $httpCode . ', 响应内容: ' . $response); } // 📝 记录成功响应日志 $totalTime = round(($curlInfo['total_time'] ?? 0) * 1000, 2); $connectTime = round(($curlInfo['connect_time'] ?? 0) * 1000, 2); $responseLength = mb_strlen($response); Log::info('建行生活API请求成功 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [http_code=' . $httpCode . '] [response_length=' . $responseLength . '] [cost_time=' . $costTime . 'ms] [total_time=' . $totalTime . 'ms] [connect_time=' . $connectTime . 'ms]'); return $response; } /** * 处理响应 * * @param string $response 原始响应内容 * @return array 解密后的响应数据 * @throws \Exception */ private function handleResponse($response) { // 📝 记录原始响应内容 Log::info('建行生活API原始响应内容: ' . $response); // 解析JSON响应 $responseData = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception('响应JSON解析失败: ' . json_last_error_msg()); } // 检查响应结构 if (!isset($responseData['cnt']) || !isset($responseData['mac'])) { throw new \Exception('响应格式错误,缺少cnt或mac字段'); } // 📝 记录加密响应参数 Log::info('加密响应参数 cnt: ' . $responseData['cnt']); Log::info('加密响应参数 mac: ' . $responseData['mac']); // 解密响应内容 $decryptedContent = CcbRSA::decryptFromCcb($responseData['cnt'], $this->config['private_key']); // 📝 记录解密后的响应内容 Log::info('解密后响应内容: ' . $decryptedContent); // 验证签名 $isValid = CcbMD5::verifyApiSignature($decryptedContent, $responseData['mac'], $this->config['private_key']); if (!$isValid) { Log::error('响应签名验证失败 [expected_mac=' . $responseData['mac'] . ']'); throw new \Exception('响应签名验证失败'); } Log::info('响应签名验证成功'); // 解析解密后的内容 $decryptedData = json_decode($decryptedContent, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception('解密后的JSON解析失败: ' . json_last_error_msg()); } // 检查业务响应码 $this->checkBusinessResponse($decryptedData); return $decryptedData; } /** * 检查业务响应码 * * @param array $data 响应数据 * @throws \Exception */ private function checkBusinessResponse($data) { // 检查响应头 if (!isset($data['CLD_HEADER']) || !isset($data['CLD_BODY'])) { throw new \Exception('响应数据结构错误'); } // 检查响应码(如果存在) if (isset($data['CLD_BODY']['CLD_RESP_CODE'])) { $respCode = $data['CLD_BODY']['CLD_RESP_CODE']; $respMsg = isset($data['CLD_BODY']['CLD_RESP_MSG']) ? $data['CLD_BODY']['CLD_RESP_MSG'] : ''; if ($respCode !== '000000') { throw new \Exception('业务处理失败[' . $respCode . ']: ' . $respMsg); } } } /** * 验证配置 * * @throws \Exception */ private function validateConfig() { $requiredFields = [ 'api_base_url', 'merchant_id', 'pos_id', 'branch_id', 'private_key', 'public_key', 'service_id', 'tx_codes' ]; foreach ($requiredFields as $field) { if (!isset($this->config[$field]) || empty($this->config[$field])) { throw new \Exception('配置缺少必要字段: ' . $field); } } // 验证交易代码配置完整性 $requiredTxCodes = ['order_push', 'order_update', 'order_query', 'order_refund']; foreach ($requiredTxCodes as $txCode) { if (!isset($this->config['tx_codes'][$txCode])) { throw new \Exception('交易代码配置缺少: ' . $txCode); } } } /** * 订单推送(A3341TP01) * * @param array $orderData 订单数据 * @return array 响应数据 * @throws \Exception */ public function pushOrder($orderData) { return $this->request($this->config['tx_codes']['order_push'], $orderData); } /** * 订单状态更新(A3341TP02) * * @param string $userId 用户ID * @param string $orderId 订单ID * @param string $orderStatus 订单状态 * @param string $refundStatus 退款状态 * @return array 响应数据 * @throws \Exception */ public function updateOrderStatus($userId, $orderId, $orderStatus, $refundStatus = '0') { $body = [ 'USER_ID' => $userId, 'ORDER_ID' => $orderId, 'ORDER_STATUS' => $orderStatus, 'REFUND_STATUS' => $refundStatus ]; return $this->request($this->config['tx_codes']['order_update'], $body); } /** * 订单查询(A3341TP03) * * @param string $onlnPyTxnOrdrId 支付订单ID * @param string $txnStatus 交易状态 * @return array 响应数据 * @throws \Exception */ public function queryOrder($onlnPyTxnOrdrId, $txnStatus = '00') { $body = [ 'ONLN_PY_TXN_ORDR_ID' => $onlnPyTxnOrdrId, 'PAGE' => '1', 'TXN_PRD_TPCD' => '06', 'TXN_STATUS' => $txnStatus, 'TX_TYPE' => '0' ]; return $this->request($this->config['tx_codes']['order_query'], $body); } /** * 退款接口(A3341TP04) * * @param string $orderId 订单ID * @param string $refundAmount 退款金额 * @param string $refundReason 退款原因 * @return array 响应数据 * @throws \Exception */ public function refund($orderId, $refundAmount, $refundReason = '') { $body = [ 'ORDER_ID' => $orderId, 'REFUND_AMOUNT' => $refundAmount, 'REFUND_REASON' => $refundReason, 'REFUND_TIME' => date('YmdHis') ]; return $this->request($this->config['tx_codes']['order_refund'], $body); } /** * 测试连接 * 使用查询接口测试连接是否正常 * * @return bool 是否连接成功 */ public function testConnection() { try { // 使用一个不存在的订单号进行查询测试 $this->queryOrder('TEST' . time()); return true; } catch (\Exception $e) { // 如果是业务错误(订单不存在),说明连接正常 if (strpos($e->getMessage(), '业务处理失败') !== false) { return true; } return false; } } }