2025-10-17 17:18:15 +08:00

333 lines
9.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace addons\shopro\library\ccblife;
/**
* 建行生活HTTP客户端
* 处理与建行API的通信包括加密、签名、发送请求和解密响应
*/
class CcbHttpClient
{
/**
* 建行API生产环境地址
*/
const API_URL = 'https://life.ccb.com/tran/merchant/channel/api.jhtml';
/**
* 默认超时时间(秒)
*/
const DEFAULT_TIMEOUT = 30;
/**
* 商户配置
*/
private $config;
/**
* 构造函数
*
* @param array $config 配置数组包含merchant_id, pos_id, branch_id, private_key, public_key, service_id
*/
public function __construct($config)
{
$this->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;
}
}
}