288 lines
8.1 KiB
PHP
Raw Normal View History

2025-10-17 16:32:16 +08:00
<?php
namespace addons\shopro\library\ccblife;
use think\Exception;
use think\Log;
/**
* 建行生活HTTP客户端类
*
* 功能:
* - 发送HTTP请求到建行
* - 处理HTTP响应
* - 失败重试机制
* - 超时控制
*
* @author Billy
* @date 2025-01-16
*/
class CcbHttpClient
{
/**
* 配置信息
* @var array
*/
private $config;
/**
* 加密实例
* @var CcbEncryption
*/
private $encryption;
/**
* 构造函数
*
* @param array $config 配置数组
*/
public function __construct($config = [])
{
if (empty($config)) {
$config = config('ccblife');
}
$this->config = $config;
$this->encryption = new CcbEncryption($config);
}
/**
* 发送请求到建行
*
* @param string $txCode 交易代码 (: A3341TP01)
* @param array $bodyData 业务数据
* @return array 解密后的响应数据
* @throws Exception
*/
public function request($txCode, $bodyData)
{
// 记录开始时间
$startTime = microtime(true);
try {
// 1. 获取接口地址
$url = $this->getApiUrl($txCode);
// 2. 构建加密报文
$requestData = $this->encryption->buildEncryptedMessage($txCode, $bodyData);
// 3. 记录请求日志
$this->logRequest($txCode, $bodyData, $requestData);
// 4. 发送HTTP请求 (带重试机制)
$response = $this->retry(function () use ($url, $requestData) {
return $this->post($url, $requestData);
});
// 5. 解析响应
$result = $this->encryption->parseResponse($response);
// 6. 记录响应日志
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->logResponse($txCode, $result, $costTime);
// 7. 检查业务返回码
$this->checkReturnCode($result);
return $result;
} catch (Exception $e) {
// 记录错误日志
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->logError($txCode, $e->getMessage(), $costTime);
throw $e;
}
}
/**
* 发送POST请求
*
* @param string $url 请求URL
* @param array $data 请求数据
* @return string 响应内容
* @throws Exception
*/
private function post($url, $data)
{
// 初始化CURL
$ch = curl_init();
// 设置CURL选项
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->config['http']['timeout'] ?? 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'Content-Type: application/json',
]);
// 如果是HTTPS,验证证书
if (strpos($url, 'https') === 0) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
}
// 执行请求
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// 检查CURL错误
if ($error) {
throw new Exception('CURL错误: ' . $error);
}
// 检查HTTP状态码
if ($httpCode == 404) {
throw new Exception('接口404,请检查请求头是否包含Accept和Content-Type');
}
if ($httpCode !== 200) {
throw new Exception('HTTP错误码: ' . $httpCode . ', 响应: ' . $response);
}
return $response;
}
/**
* 重试机制
*
* @param callable $callable 要执行的函数
* @param int|null $maxRetries 最大重试次数
* @return mixed 执行结果
* @throws Exception
*/
private function retry($callable, $maxRetries = null)
{
if ($maxRetries === null) {
$maxRetries = $this->config['http']['retry_times'] ?? 3;
}
$delays = $this->config['http']['retry_delay'] ?? [1, 2, 5];
$lastException = null;
for ($i = 0; $i <= $maxRetries; $i++) {
try {
return $callable();
} catch (Exception $e) {
$lastException = $e;
// 如果是最后一次尝试,不再重试
if ($i >= $maxRetries) {
break;
}
// 等待后重试
$delay = $delays[$i] ?? 5;
$this->logRetry($i + 1, $maxRetries, $delay, $e->getMessage());
sleep($delay);
}
}
throw new Exception('请求失败,已重试' . $maxRetries . '次: ' . $lastException->getMessage());
}
/**
* 获取API地址
*
* @param string $txCode 交易代码
* @return string 完整API地址
*/
private function getApiUrl($txCode)
{
$baseUrl = $this->config['api_base_url'] ?? '';
if (empty($baseUrl)) {
throw new Exception('API基础地址未配置');
}
return $baseUrl . '?txcode=' . $txCode;
}
/**
* 检查业务返回码
*
* @param array $result 响应数据
* @throws Exception
*/
private function checkReturnCode($result)
{
// 检查CLD_HEADER中的RET_CODE
$retCode = $result['CLD_HEADER']['RET_CODE'] ?? '';
$retMsg = $result['CLD_HEADER']['RET_MSG'] ?? '未知错误';
if ($retCode !== '000000') {
throw new Exception('建行接口返回错误[' . $retCode . ']: ' . $retMsg);
}
}
/**
* 记录请求日志
*
* @param string $txCode 交易代码
* @param array $bodyData 业务数据
* @param array $requestData 加密后的请求数据
*/
private function logRequest($txCode, $bodyData, $requestData)
{
if (!($this->config['log']['enabled'] ?? true)) {
return;
}
Log::info('[建行请求] ' . $txCode . ' svcid:' . $requestData['svcid'] . ' mac:' . $requestData['mac'] . ' cnt_length:' . strlen($requestData['cnt']) . ' body_data:' . json_encode($bodyData, JSON_UNESCAPED_UNICODE));
}
/**
* 记录响应日志
*
* @param string $txCode 交易代码
* @param array $result 响应数据
* @param float $costTime 耗时(毫秒)
*/
private function logResponse($txCode, $result, $costTime)
{
if (!($this->config['log']['enabled'] ?? true)) {
return;
}
Log::info('[建行响应] ' . $txCode . ' ret_code:' . ($result['CLD_HEADER']['RET_CODE'] ?? '') . ' ret_msg:' . ($result['CLD_HEADER']['RET_MSG'] ?? '') . ' cost_time:' . $costTime . 'ms');
}
/**
* 记录错误日志
*
* @param string $txCode 交易代码
* @param string $errorMsg 错误信息
* @param float $costTime 耗时(毫秒)
*/
private function logError($txCode, $errorMsg, $costTime)
{
if (!($this->config['log']['enabled'] ?? true)) {
return;
}
Log::error('[建行错误] ' . $txCode . ' error:' . $errorMsg . ' cost_time:' . $costTime . 'ms');
}
/**
* 记录重试日志
*
* @param int $currentRetry 当前重试次数
* @param int $maxRetries 最大重试次数
* @param int $delay 延迟秒数
* @param string $reason 重试原因
*/
private function logRetry($currentRetry, $maxRetries, $delay, $reason)
{
if (!($this->config['log']['enabled'] ?? true)) {
return;
}
Log::warning('[建行重试] retry:' . $currentRetry . '/' . $maxRetries . ' delay:' . $delay . 's reason:' . $reason);
}
}