404 lines
14 KiB
PHP
Raw Normal View History

2025-10-17 16:32:16 +08:00
<?php
namespace addons\shopro\library\ccblife;
2025-10-21 11:11:59 +08:00
use think\Log;
2025-10-17 16:32:16 +08:00
/**
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-21 11:11:59 +08:00
// 📝 记录原始请求报文(加密前)
Log::info('建行生活API原始请求报文 [txCode=' . $txCode . '] [txSeq=' . $txSeq . ']');
Log::info('原始报文内容: ' . $message);
2025-10-22 11:34:32 +08:00
// 使用商户公钥加密请求报文
$encryptPublicKey = $this->config['public_key'];
if (empty($encryptPublicKey)) {
throw new \Exception('RSA公钥未配置请检查.env中的public_key配置');
}
Log::info('使用公钥加密(前64字符): ' . substr($encryptPublicKey, 0, 64));
2025-10-22 14:59:47 +08:00
// 第一次加密和BASE64编码
2025-10-22 11:34:32 +08:00
$encryptedMessage = CcbRSA::encryptForCcb($message, $encryptPublicKey);
2025-10-17 16:32:16 +08:00
2025-10-22 14:59:47 +08:00
// 第二次BASE64编码按照建行demo要求
// demo1.java第120行: enc_msg = encoder.encode(enc_msg.getBytes("UTF-8"));
$encryptedMessage = base64_encode($encryptedMessage);
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-22 11:34:32 +08:00
// ✅ 使用商户私钥签名
2025-10-17 17:18:15 +08:00
$mac = CcbMD5::signApiMessage($message, $this->config['private_key']);
2025-10-22 11:34:32 +08:00
Log::info('生成的MAC签名: ' . $mac);
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-21 11:11:59 +08:00
// 记录请求开始时间
$startTime = microtime(true);
2025-10-18 15:47:25 +08:00
// 构建完整的API URL基础URL + ?txcode=交易代码)
$apiUrl = $this->config['api_base_url'] . '?txcode=' . $txCode;
2025-10-21 15:03:39 +08:00
// 构建请求参数按照建行报文规范cnt、mac、svcid
2025-10-17 17:18:15 +08:00
$params = [
'cnt' => $cnt,
2025-10-21 15:03:39 +08:00
'mac' => $mac,
'svcid' => $this->config['service_id']
2025-10-17 17:18:15 +08:00
];
2025-10-21 20:48:28 +08:00
// ✅ 将请求参数转为JSON格式按照建行报文示例要求
$jsonParams = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
2025-10-21 11:11:59 +08:00
// 📝 记录请求参数(加密后的完整内容)
2025-10-21 20:48:28 +08:00
Log::info('建行生活API加密请求 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [timeout=' . self::DEFAULT_TIMEOUT . 's]');
Log::info('JSON请求体: ' . $jsonParams);
2025-10-21 11:11:59 +08:00
2025-10-17 16:32:16 +08:00
// 初始化CURL
$ch = curl_init();
2025-10-21 20:48:28 +08:00
// 设置CURL选项按照建行要求发送JSON格式
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,
2025-10-21 20:48:28 +08:00
CURLOPT_POSTFIELDS => $jsonParams, // ✅ 发送JSON格式不是URL编码
2025-10-17 17:18:15 +08:00
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => self::DEFAULT_TIMEOUT,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
2025-10-21 20:48:28 +08:00
'Content-Type: application/json; charset=utf-8', // ✅ JSON格式
2025-10-17 17:18:15 +08:00
'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-21 11:11:59 +08:00
$curlInfo = curl_getinfo($ch);
2025-10-17 16:32:16 +08:00
curl_close($ch);
2025-10-21 11:11:59 +08:00
// 计算请求耗时
$costTime = round((microtime(true) - $startTime) * 1000, 2); // 毫秒
2025-10-17 17:18:15 +08:00
// 检查错误
2025-10-17 16:32:16 +08:00
if ($error) {
2025-10-21 11:11:59 +08:00
// 📝 记录错误日志
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]');
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-21 11:11:59 +08:00
// 📝 记录HTTP状态码异常日志
$responsePreview = mb_substr($response, 0, 300);
Log::error('建行生活API响应状态码异常 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [http_code=' . $httpCode . '] [cost_time=' . $costTime . 'ms] [response=' . $responsePreview . ']');
2025-10-17 17:18:15 +08:00
throw new \Exception('HTTP状态码异常: ' . $httpCode . ', 响应内容: ' . $response);
2025-10-17 16:32:16 +08:00
}
2025-10-21 11:11:59 +08:00
// 📝 记录成功响应日志
$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]');
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-21 11:11:59 +08:00
// 📝 记录原始响应内容
Log::info('建行生活API原始响应内容: ' . $response);
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-21 11:11:59 +08:00
// 📝 记录加密响应参数
Log::info('加密响应参数 cnt: ' . $responseData['cnt']);
Log::info('加密响应参数 mac: ' . $responseData['mac']);
2025-10-22 14:59:47 +08:00
// 第一次BASE64解码按照建行demo要求
// demo1.java第139行: enc_msg = new String(decoder.decodeBuffer(enc_msg),"UTF-8");
$cntDecoded = base64_decode($responseData['cnt']);
// 第二次解密和BASE64解码
$decryptedContent = CcbRSA::decryptFromCcb($cntDecoded, $this->config['private_key']);
2025-10-17 16:32:16 +08:00
2025-10-21 11:11:59 +08:00
// 📝 记录解密后的响应内容
Log::info('解密后响应内容: ' . $decryptedContent);
2025-10-17 17:18:15 +08:00
// 验证签名
$isValid = CcbMD5::verifyApiSignature($decryptedContent, $responseData['mac'], $this->config['private_key']);
if (!$isValid) {
2025-10-21 11:11:59 +08:00
Log::error('响应签名验证失败 [expected_mac=' . $responseData['mac'] . ']');
2025-10-17 17:18:15 +08:00
throw new \Exception('响应签名验证失败');
}
2025-10-17 16:32:16 +08:00
2025-10-21 11:11:59 +08:00
Log::info('响应签名验证成功');
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
}
}
}