2025-10-17 16:32:16 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace addons\shopro\library\ccblife;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 建行生活HTTP客户端
|
|
|
|
|
|
* 处理与建行API的通信,包括加密、签名、发送请求和解密响应
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
|
|
|
|
|
class CcbHttpClient
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 默认超时时间(秒)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
const DEFAULT_TIMEOUT = 30;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 商户配置
|
|
|
|
|
|
*/
|
|
|
|
|
|
private $config;
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构造函数
|
|
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param array $config 配置数组,包含merchant_id, pos_id, branch_id, private_key, public_key, service_id
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
public function __construct($config)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
$this->config = $config;
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$this->validateConfig();
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 发送API请求
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param string $txCode 交易码(如svc_occMebOrderPush)
|
|
|
|
|
|
* @param array $body 请求体数据
|
|
|
|
|
|
* @param string $txSeq 交易流水号,不传则自动生成
|
|
|
|
|
|
* @return array 响应数据
|
|
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
public function request($txCode, $body, $txSeq = null)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 生成交易流水号
|
|
|
|
|
|
if (empty($txSeq)) {
|
|
|
|
|
|
$txSeq = CcbMD5::generateTransactionSeq();
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 构建请求报文
|
|
|
|
|
|
$message = $this->buildMessage($txCode, $body, $txSeq);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 加密报文
|
|
|
|
|
|
$encryptedMessage = CcbRSA::encryptForCcb($message, $this->config['public_key']);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 移除BASE64中的换行符
|
|
|
|
|
|
$encryptedMessage = str_replace(["\r", "\n", "\r\n"], '', $encryptedMessage);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 生成签名
|
|
|
|
|
|
$mac = CcbMD5::signApiMessage($message, $this->config['private_key']);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 发送HTTP请求
|
2025-10-18 15:47:25 +08:00
|
|
|
|
$response = $this->sendHttpRequest($txCode, $encryptedMessage, $mac);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 处理响应
|
|
|
|
|
|
return $this->handleResponse($response);
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 构建请求报文
|
|
|
|
|
|
*
|
|
|
|
|
|
* @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);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 发送HTTP请求
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-18 15:47:25 +08:00
|
|
|
|
* @param string $txCode 交易代码(用于构建URL)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param string $cnt 加密后的报文内容
|
|
|
|
|
|
* @param string $mac 签名
|
2025-10-17 16:32:16 +08:00
|
|
|
|
* @return string 响应内容
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-18 15:47:25 +08:00
|
|
|
|
private function sendHttpRequest($txCode, $cnt, $mac)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-18 15:47:25 +08:00
|
|
|
|
// 构建完整的API URL(基础URL + ?txcode=交易代码)
|
|
|
|
|
|
$apiUrl = $this->config['api_base_url'] . '?txcode=' . $txCode;
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 构建请求参数
|
|
|
|
|
|
$params = [
|
|
|
|
|
|
'cnt' => $cnt,
|
|
|
|
|
|
'mac' => $mac
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-17 16:32:16 +08:00
|
|
|
|
// 初始化CURL
|
|
|
|
|
|
$ch = curl_init();
|
|
|
|
|
|
|
|
|
|
|
|
// 设置CURL选项
|
2025-10-17 17:18:15 +08:00
|
|
|
|
curl_setopt_array($ch, [
|
2025-10-18 15:47:25 +08:00
|
|
|
|
CURLOPT_URL => $apiUrl,
|
2025-10-17 17:18:15 +08:00
|
|
|
|
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'
|
|
|
|
|
|
]
|
2025-10-17 16:32:16 +08:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 执行请求
|
|
|
|
|
|
$response = curl_exec($ch);
|
|
|
|
|
|
$error = curl_error($ch);
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
curl_close($ch);
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 检查错误
|
2025-10-17 16:32:16 +08:00
|
|
|
|
if ($error) {
|
2025-10-17 17:18:15 +08:00
|
|
|
|
throw new \Exception('HTTP请求失败: ' . $error);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($httpCode !== 200) {
|
2025-10-17 17:18:15 +08:00
|
|
|
|
throw new \Exception('HTTP状态码异常: ' . $httpCode . ', 响应内容: ' . $response);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 处理响应
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param string $response 原始响应内容
|
|
|
|
|
|
* @return array 解密后的响应数据
|
|
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
private function handleResponse($response)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 解析JSON响应
|
|
|
|
|
|
$responseData = json_decode($response, true);
|
|
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
|
|
|
throw new \Exception('响应JSON解析失败: ' . json_last_error_msg());
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 检查响应结构
|
|
|
|
|
|
if (!isset($responseData['cnt']) || !isset($responseData['mac'])) {
|
|
|
|
|
|
throw new \Exception('响应格式错误,缺少cnt或mac字段');
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 解密响应内容
|
|
|
|
|
|
$decryptedContent = CcbRSA::decryptFromCcb($responseData['cnt'], $this->config['private_key']);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 验证签名
|
|
|
|
|
|
$isValid = CcbMD5::verifyApiSignature($decryptedContent, $responseData['mac'], $this->config['private_key']);
|
|
|
|
|
|
if (!$isValid) {
|
|
|
|
|
|
throw new \Exception('响应签名验证失败');
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 解析解密后的内容
|
|
|
|
|
|
$decryptedData = json_decode($decryptedContent, true);
|
|
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
|
|
|
|
throw new \Exception('解密后的JSON解析失败: ' . json_last_error_msg());
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 检查业务响应码
|
|
|
|
|
|
$this->checkBusinessResponse($decryptedData);
|
|
|
|
|
|
|
|
|
|
|
|
return $decryptedData;
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 检查业务响应码
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param array $data 响应数据
|
|
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
private function checkBusinessResponse($data)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 检查响应头
|
|
|
|
|
|
if (!isset($data['CLD_HEADER']) || !isset($data['CLD_BODY'])) {
|
|
|
|
|
|
throw new \Exception('响应数据结构错误');
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 检查响应码(如果存在)
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 验证配置
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
private function validateConfig()
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$requiredFields = [
|
2025-10-18 15:47:25 +08:00
|
|
|
|
'api_base_url',
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'merchant_id',
|
|
|
|
|
|
'pos_id',
|
|
|
|
|
|
'branch_id',
|
|
|
|
|
|
'private_key',
|
|
|
|
|
|
'public_key',
|
2025-10-18 15:47:25 +08:00
|
|
|
|
'service_id',
|
|
|
|
|
|
'tx_codes'
|
2025-10-17 17:18:15 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($requiredFields as $field) {
|
|
|
|
|
|
if (!isset($this->config[$field]) || empty($this->config[$field])) {
|
|
|
|
|
|
throw new \Exception('配置缺少必要字段: ' . $field);
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
2025-10-18 15:47:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 验证交易代码配置完整性
|
|
|
|
|
|
$requiredTxCodes = ['order_push', 'order_update', 'order_query', 'order_refund'];
|
|
|
|
|
|
foreach ($requiredTxCodes as $txCode) {
|
|
|
|
|
|
if (!isset($this->config['tx_codes'][$txCode])) {
|
|
|
|
|
|
throw new \Exception('交易代码配置缺少: ' . $txCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 订单推送(A3341TP01)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param array $orderData 订单数据
|
|
|
|
|
|
* @return array 响应数据
|
|
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
public function pushOrder($orderData)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-18 15:47:25 +08:00
|
|
|
|
return $this->request($this->config['tx_codes']['order_push'], $orderData);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 订单状态更新(A3341TP02)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param string $userId 用户ID
|
|
|
|
|
|
* @param string $orderId 订单ID
|
|
|
|
|
|
* @param string $orderStatus 订单状态
|
|
|
|
|
|
* @param string $refundStatus 退款状态
|
|
|
|
|
|
* @return array 响应数据
|
|
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
public function updateOrderStatus($userId, $orderId, $orderStatus, $refundStatus = '0')
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$body = [
|
|
|
|
|
|
'USER_ID' => $userId,
|
|
|
|
|
|
'ORDER_ID' => $orderId,
|
|
|
|
|
|
'ORDER_STATUS' => $orderStatus,
|
|
|
|
|
|
'REFUND_STATUS' => $refundStatus
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-18 15:47:25 +08:00
|
|
|
|
return $this->request($this->config['tx_codes']['order_update'], $body);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 订单查询(A3341TP03)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param string $onlnPyTxnOrdrId 支付订单ID
|
|
|
|
|
|
* @param string $txnStatus 交易状态
|
|
|
|
|
|
* @return array 响应数据
|
|
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
public function queryOrder($onlnPyTxnOrdrId, $txnStatus = '00')
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$body = [
|
|
|
|
|
|
'ONLN_PY_TXN_ORDR_ID' => $onlnPyTxnOrdrId,
|
|
|
|
|
|
'PAGE' => '1',
|
|
|
|
|
|
'TXN_PRD_TPCD' => '06',
|
|
|
|
|
|
'TXN_STATUS' => $txnStatus,
|
|
|
|
|
|
'TX_TYPE' => '0'
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-18 15:47:25 +08:00
|
|
|
|
return $this->request($this->config['tx_codes']['order_query'], $body);
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 退款接口(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')
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-18 15:47:25 +08:00
|
|
|
|
return $this->request($this->config['tx_codes']['order_refund'], $body);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 测试连接
|
|
|
|
|
|
* 使用查询接口测试连接是否正常
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @return bool 是否连接成功
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
public function testConnection()
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-17 17:18:15 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 使用一个不存在的订单号进行查询测试
|
|
|
|
|
|
$this->queryOrder('TEST' . time());
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
// 如果是业务错误(订单不存在),说明连接正常
|
|
|
|
|
|
if (strpos($e->getMessage(), '业务处理失败') !== false) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|