mirror of
https://gitee.com/liuxioabin/fengketrade.git
synced 2026-04-17 21:03:17 +08:00
288 lines
8.1 KiB
PHP
288 lines
8.1 KiB
PHP
|
|
<?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);
|
||
|
|
}
|
||
|
|
}
|