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
|
|
|
|
// 检查业务响应码
|
2025-10-22 19:55:52 +08:00
|
|
|
|
$this->checkBusinessResponse($decryptedData);
|
2025-10-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
// 检查响应头
|
2025-10-22 15:14:24 +08:00
|
|
|
|
if (!isset($data['CLD_HEADER'])) {
|
|
|
|
|
|
throw new \Exception('响应数据结构错误:缺少CLD_HEADER');
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 19:55:52 +08:00
|
|
|
|
// 检查 CLD_HEADER.CLD_TX_RESP(建行标准响应格式)
|
2025-10-22 15:14:24 +08:00
|
|
|
|
if (isset($data['CLD_HEADER']['CLD_TX_RESP'])) {
|
|
|
|
|
|
$txResp = $data['CLD_HEADER']['CLD_TX_RESP'];
|
2025-10-22 19:55:52 +08:00
|
|
|
|
$code = isset($txResp['CLD_CODE']) ? $txResp['CLD_CODE'] : 'UNKNOWN';
|
|
|
|
|
|
$desc = isset($txResp['CLD_DESC']) ? $txResp['CLD_DESC'] : '未知错误';
|
2025-10-17 17:18:15 +08:00
|
|
|
|
|
2025-10-22 19:55:52 +08:00
|
|
|
|
// 只有非成功状态才抛出异常
|
|
|
|
|
|
if ($code !== 'CLD_SUCCESS') {
|
|
|
|
|
|
throw new \Exception('建行业务错误[' . $code . ']: ' . $desc);
|
|
|
|
|
|
}
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
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-22 20:32:53 +08:00
|
|
|
|
* @param string $orderId 订单编号(用户订单号,对应收银台USER_ORDERID字段)
|
|
|
|
|
|
* @param string $informId 通知类型(0-支付状态修改 1-退款状态修改)
|
|
|
|
|
|
* @param string $payFlowId 支付流水号(对应收银台ORDERID字段)
|
|
|
|
|
|
* @param string $payMrchId 支付商户号
|
|
|
|
|
|
* @param array $additionalParams 额外参数(PAY_STATUS、REFUND_STATUS、PAY_AMT、DISCOUNT_AMT、CUS_ORDER_URL等)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @return array 响应数据
|
|
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-22 20:32:53 +08:00
|
|
|
|
public function updateOrderStatus($orderId, $informId, $payFlowId, $payMrchId, $additionalParams = [])
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-22 20:32:53 +08:00
|
|
|
|
// 验证通知类型
|
|
|
|
|
|
if (!in_array($informId, ['0', '1'])) {
|
|
|
|
|
|
throw new \Exception('通知类型INFORM_ID必须为0(支付状态修改)或1(退款状态修改)');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证互斥规则
|
|
|
|
|
|
if ($informId == '0') {
|
|
|
|
|
|
// 支付状态修改时,PAY_STATUS必填,REFUND_STATUS为空
|
|
|
|
|
|
if (empty($additionalParams['PAY_STATUS'])) {
|
|
|
|
|
|
throw new \Exception('支付状态修改时PAY_STATUS不能为空');
|
|
|
|
|
|
}
|
|
|
|
|
|
$additionalParams['REFUND_STATUS'] = null;
|
|
|
|
|
|
} elseif ($informId == '1') {
|
|
|
|
|
|
// 退款状态修改时,REFUND_STATUS必填,PAY_STATUS为空
|
|
|
|
|
|
if (empty($additionalParams['REFUND_STATUS'])) {
|
|
|
|
|
|
throw new \Exception('退款状态修改时REFUND_STATUS不能为空');
|
|
|
|
|
|
}
|
|
|
|
|
|
$additionalParams['PAY_STATUS'] = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建请求体(必填字段)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$body = [
|
|
|
|
|
|
'ORDER_ID' => $orderId,
|
2025-10-22 20:32:53 +08:00
|
|
|
|
'INFORM_ID' => $informId,
|
|
|
|
|
|
'PAY_FLOW_ID' => $payFlowId,
|
|
|
|
|
|
'PAY_MRCH_ID' => $payMrchId
|
2025-10-17 17:18:15 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-22 20:32:53 +08:00
|
|
|
|
// 合并额外参数
|
|
|
|
|
|
$body = array_merge($body, $additionalParams);
|
|
|
|
|
|
|
|
|
|
|
|
// 移除空值字段
|
|
|
|
|
|
$body = array_filter($body, function($value) {
|
|
|
|
|
|
return $value !== null && $value !== '';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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-22 20:32:53 +08:00
|
|
|
|
* @param string $onlnPyTxnOrdrId 订单编号(调用收银台时支付流水号,对应字段ORDERID)
|
|
|
|
|
|
* @param string|null $startTime 开始日期时间(格式yyyyMMddHHmmss,默认为7天前)
|
|
|
|
|
|
* @param string|null $endTime 结束日期时间(格式yyyyMMddHHmmss,默认为当前时间)
|
|
|
|
|
|
* @param int $page 当前页次(默认1)
|
|
|
|
|
|
* @param string $txType 交易类型(0-支付交易 1-退款交易 a-查询可退款的订单)
|
|
|
|
|
|
* @param string $txnStatus 交易状态(00-交易成功 01-交易失败 02-不确定)
|
|
|
|
|
|
* @param array $additionalParams 额外参数(PLAT_MCT_ID、CUSTOMERID、BRANCHID、SCN_IDR等)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @return array 响应数据
|
|
|
|
|
|
* @throws \Exception
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-22 20:32:53 +08:00
|
|
|
|
public function queryOrder($onlnPyTxnOrdrId, $startTime = null, $endTime = null, $page = 1, $txType = '0', $txnStatus = '00', $additionalParams = [])
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-22 20:32:53 +08:00
|
|
|
|
// 默认查询最近7天
|
|
|
|
|
|
if (empty($startTime)) {
|
|
|
|
|
|
$startTime = date('YmdHis', strtotime('-7 days'));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (empty($endTime)) {
|
|
|
|
|
|
$endTime = date('YmdHis');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建请求体(必填字段)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$body = [
|
2025-10-22 20:32:53 +08:00
|
|
|
|
'TX_TYPE' => $txType,
|
|
|
|
|
|
'TXN_PRD_TPCD' => '99', // 99-自定义时间段查询(文档要求)
|
|
|
|
|
|
'STDT_TM' => $startTime,
|
|
|
|
|
|
'EDDT_TM' => $endTime,
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'ONLN_PY_TXN_ORDR_ID' => $onlnPyTxnOrdrId,
|
|
|
|
|
|
'TXN_STATUS' => $txnStatus,
|
2025-10-22 20:32:53 +08:00
|
|
|
|
'PAGE' => (string)$page
|
2025-10-17 17:18:15 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-22 20:32:53 +08:00
|
|
|
|
// 合并额外参数(商户信息等)
|
|
|
|
|
|
$body = array_merge($body, $additionalParams);
|
|
|
|
|
|
|
|
|
|
|
|
// 移除空值字段
|
|
|
|
|
|
$body = array_filter($body, function($value) {
|
|
|
|
|
|
return $value !== null && $value !== '';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
*
|
2025-10-22 20:32:53 +08:00
|
|
|
|
* @param string $orderId 订单号(调用收银台时支付流水号,对应字段ORDERID)
|
|
|
|
|
|
* @param float|string $refundAmount 退款金额(单位:元)
|
|
|
|
|
|
* @param string|int $payTime 支付时间(时间戳或日期时间字符串)
|
|
|
|
|
|
* @param string|null $refundCode 退款流水号(可选,建议填写用于查询退款结果)
|
|
|
|
|
|
* @param array $additionalParams 额外参数(PLAT_MCT_ID、CUSTOMERID、BRANCHID等)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @return array 响应数据
|
|
|
|
|
|
* @throws \Exception
|
|
|
|
|
|
*/
|
2025-10-22 20:32:53 +08:00
|
|
|
|
public function refund($orderId, $refundAmount, $payTime, $refundCode = null, $additionalParams = [])
|
2025-10-17 17:18:15 +08:00
|
|
|
|
{
|
2025-10-22 20:32:53 +08:00
|
|
|
|
// 计算时间范围(支付时间前后4小时)
|
|
|
|
|
|
$payTimestamp = is_numeric($payTime) ? $payTime : strtotime($payTime);
|
|
|
|
|
|
if (!$payTimestamp) {
|
|
|
|
|
|
throw new \Exception('支付时间格式错误,请传入时间戳或有效的日期时间字符串');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$stat_tm = date('YmdHis', $payTimestamp - 4*3600); // 支付时间往前4小时
|
|
|
|
|
|
$edit_tm = date('YmdHis', min($payTimestamp + 4*3600, time())); // 支付时间往后4小时,但不超过当前时间
|
|
|
|
|
|
|
|
|
|
|
|
// 生成退款流水号(如果未提供)
|
|
|
|
|
|
if (empty($refundCode)) {
|
|
|
|
|
|
$refundCode = 'RF' . date('YmdHis') . str_pad(mt_rand(0, 9999), 4, '0', STR_PAD_LEFT);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化退款金额(保留2位小数)
|
|
|
|
|
|
$refundAmount = number_format((float)$refundAmount, 2, '.', '');
|
|
|
|
|
|
|
|
|
|
|
|
// 构建请求体(必填字段)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$body = [
|
2025-10-22 20:32:53 +08:00
|
|
|
|
'ORDER' => $orderId, // 注意:字段名是 ORDER,不是 ORDER_ID
|
|
|
|
|
|
'MONEY' => $refundAmount, // 注意:字段名是 MONEY,不是 REFUND_AMOUNT
|
|
|
|
|
|
'STDT_TM' => $stat_tm,
|
|
|
|
|
|
'EDDT_TM' => $edit_tm,
|
|
|
|
|
|
'REFUND_CODE' => $refundCode
|
2025-10-17 17:18:15 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-22 20:32:53 +08:00
|
|
|
|
// 合并额外参数(商户信息等)
|
|
|
|
|
|
$body = array_merge($body, $additionalParams);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-22 20:32:53 +08:00
|
|
|
|
// 移除空值字段
|
|
|
|
|
|
$body = array_filter($body, function($value) {
|
|
|
|
|
|
return $value !== null && $value !== '';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return $this->request($this->config['tx_codes']['order_refund'], $body);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|