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); // 加密报文 $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($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 $cnt 加密后的报文内容 * @param string $mac 签名 * @return string 响应内容 * @throws \Exception */ private function sendHttpRequest($cnt, $mac) { // 构建请求参数 $params = [ 'cnt' => $cnt, 'mac' => $mac ]; // 初始化CURL $ch = curl_init(); // 设置CURL选项 curl_setopt_array($ch, [ CURLOPT_URL => self::API_URL, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($params), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => self::DEFAULT_TIMEOUT, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HTTPHEADER => [ 'Content-Type: application/x-www-form-urlencoded', 'Accept: application/json' ] ]); // 执行请求 $response = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); // 检查错误 if ($error) { throw new \Exception('HTTP请求失败: ' . $error); } if ($httpCode !== 200) { throw new \Exception('HTTP状态码异常: ' . $httpCode . ', 响应内容: ' . $response); } return $response; } /** * 处理响应 * * @param string $response 原始响应内容 * @return array 解密后的响应数据 * @throws \Exception */ private function handleResponse($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字段'); } // 解密响应内容 $decryptedContent = CcbRSA::decryptFromCcb($responseData['cnt'], $this->config['private_key']); // 验证签名 $isValid = CcbMD5::verifyApiSignature($decryptedContent, $responseData['mac'], $this->config['private_key']); if (!$isValid) { throw new \Exception('响应签名验证失败'); } // 解析解密后的内容 $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 = [ 'merchant_id', 'pos_id', 'branch_id', 'private_key', 'public_key', 'service_id' ]; foreach ($requiredFields as $field) { if (!isset($this->config[$field]) || empty($this->config[$field])) { throw new \Exception('配置缺少必要字段: ' . $field); } } } /** * 订单推送(A3341TP01) * * @param array $orderData 订单数据 * @return array 响应数据 * @throws \Exception */ public function pushOrder($orderData) { return $this->request('svc_occMebOrderPush', $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('svc_occMebOrderStatusUpdate', $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('svc_occPlatOrderQry', $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('svc_occRefund', $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; } } }