This commit is contained in:
Billy 2025-10-21 10:17:40 +08:00
parent a4b8b710d5
commit 184be5a1d1
4 changed files with 69 additions and 117 deletions

View File

@ -7,12 +7,19 @@ use think\Exception;
/** /**
* 建行生活加密解密核心类 * 建行生活加密解密核心类
* *
* 功能: * ⚠️ 已废弃:请使用 CcbRSA、CcbMD5、CcbHttpClient 类替代
* - RSA加密与解密
* - MD5签名生成与验证
* - 报文构造与解析
* - 交易流水号生成
* *
* 废弃原因:
* 1. formatKey() 方法存在密钥格式化错误PKCS#1 vs PKCS#8 混淆)
* 2. chunk_split() 使用不当导致 OpenSSL ASN1 解析错误
* 3. CcbRSA 类功能重复,维护成本高
*
* 迁移指南:
* - RSA加密/解密 使用 CcbRSA::encrypt() / CcbRSA::decrypt()
* - MD5签名 使用 CcbMD5::signApiMessage() / CcbMD5::verifyApiSignature()
* - 加密商户公钥 CcbPaymentService 中使用 encryptPublicKeyLast30()
*
* @deprecated 2025-01-21 统一使用 CcbRSA、CcbMD5
* @author Billy * @author Billy
* @date 2025-01-16 * @date 2025-01-16
*/ */

View File

@ -34,50 +34,13 @@ class CcbOrderService
throw new \Exception('建行生活配置文件不存在'); throw new \Exception('建行生活配置文件不存在');
} }
// 处理BASE64格式的密钥 // ✅ 修复: 删除processPemKeys()调用
$this->config = $this->processPemKeys($this->config); // 密钥格式化统一由CcbRSA类处理避免重复格式化导致OpenSSL ASN1解析错误
// CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式
$this->httpClient = new CcbHttpClient($this->config); $this->httpClient = new CcbHttpClient($this->config);
} }
/**
* 处理PEM格式密钥
*
* @param array $config
* @return array
*/
private function processPemKeys($config)
{
if (!empty($config['private_key']) && strpos($config['private_key'], '-----BEGIN') === false) {
$config['private_key'] = "-----BEGIN PRIVATE KEY-----\n"
. chunk_split($config['private_key'], 64, "\n")
. "-----END PRIVATE KEY-----";
}
if (!empty($config['public_key']) && strpos($config['public_key'], '-----BEGIN') === false) {
$config['public_key'] = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split($config['public_key'], 64, "\n")
. "-----END PUBLIC KEY-----";
}
if (empty($config['merchant_public_key'])) {
$config['merchant_public_key'] = $config['public_key'];
}
// 处理平台公钥
if (!empty($config['platform_public_key'])) {
if (strpos($config['platform_public_key'], '-----BEGIN') === false) {
$config['platform_public_key'] = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split($config['platform_public_key'], 64, "\n")
. "-----END PUBLIC KEY-----";
}
} else {
$config['platform_public_key'] = $config['public_key'];
}
return $config;
}
/** /**
* 推送订单到建行生活平台 * 推送订单到建行生活平台
* 当用户下单后调用此方法同步订单信息 * 当用户下单后调用此方法同步订单信息
@ -112,23 +75,10 @@ class CcbOrderService
// 获取订单商品列表 // 获取订单商品列表
$orderItems = Db::name('shopro_order_item') $orderItems = Db::name('shopro_order_item')
->where('order_id', $orderId) ->where('order_id', $orderId)
->select() ->select();
->toArray();
// 获取支付流水号PAY_FLOW_ID必填字段
$payInfo = Db::name('shopro_pay')
->where('order_id', $orderId)
->where('status', 'paid')
->find();
if (!$payInfo || empty($payInfo['pay_sn'])) {
throw new \Exception('订单未支付或支付流水号不存在');
}
$payFlowId = $payInfo['pay_sn'];
// 构建订单数据符合A3341TP01接口规范 // 构建订单数据符合A3341TP01接口规范
$orderData = $this->buildOrderData($order, $orderItems, $ccbUserId, $payFlowId); $orderData = $this->buildOrderData($order, $orderItems, $ccbUserId);
// 记录请求数据(同步日志) // 记录请求数据(同步日志)
$txSeq = CcbMD5::generateTransactionSeq(); $txSeq = CcbMD5::generateTransactionSeq();
@ -355,10 +305,9 @@ class CcbOrderService
* @param array $order 订单数组 * @param array $order 订单数组
* @param array $orderItems 订单商品列表 * @param array $orderItems 订单商品列表
* @param string $ccbUserId 建行用户ID * @param string $ccbUserId 建行用户ID
* @param string $payFlowId 支付流水号从shopro_pay表获取
* @return array * @return array
*/ */
private function buildOrderData($order, $orderItems, $ccbUserId, $payFlowId) private function buildOrderData($order, $orderItems, $ccbUserId)
{ {
// 构建SKU商品列表JSON字符串格式 // 构建SKU商品列表JSON字符串格式
$skuList = $this->buildSkuList($orderItems); $skuList = $this->buildSkuList($orderItems);
@ -382,7 +331,7 @@ class CcbOrderService
'REFUND_STATUS' => $this->mapRefundStatus($order['refund_status'] ?? 0), // 退款状态 'REFUND_STATUS' => $this->mapRefundStatus($order['refund_status'] ?? 0), // 退款状态
'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称 'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称
'CUS_ORDER_URL' => $this->config['merchant']['order_detail_url'] . $order['id'], // 订单详情链接 'CUS_ORDER_URL' => $this->config['merchant']['order_detail_url'] . $order['id'], // 订单详情链接
'PAY_FLOW_ID' => $payFlowId, // 支付流水号(必填!) 'PAY_FLOW_ID' => $order['ccb_pay_flow_id'], // 支付流水号(必填!)
'PAY_MRCH_ID' => $this->config['merchant_id'], // 支付商户号(必填!) 'PAY_MRCH_ID' => $this->config['merchant_id'], // 支付商户号(必填!)
'SKU_LIST' => $skuList, // 商品信息JSON字符串必填 'SKU_LIST' => $skuList, // 商品信息JSON字符串必填
// ========== 非必填字段 ========== // ========== 非必填字段 ==========

View File

@ -35,56 +35,13 @@ class CcbPaymentService
throw new \Exception('建行生活配置文件不存在'); throw new \Exception('建行生活配置文件不存在');
} }
// 处理BASE64格式的密钥添加PEM包装 // ✅ 修复: 删除processPemKeys()调用
$this->config = $this->processPemKeys($this->config); // 密钥格式化统一由CcbRSA类处理避免重复格式化导致OpenSSL ASN1解析错误
// CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式
$this->orderService = new CcbOrderService(); $this->orderService = new CcbOrderService();
} }
/**
* 处理PEM格式密钥
* 如果密钥是BASE64格式不含-----BEGIN-----则添加PEM包装
*
* @param array $config 配置数组
* @return array
*/
private function processPemKeys($config)
{
// 处理私钥
if (!empty($config['private_key']) && strpos($config['private_key'], '-----BEGIN') === false) {
$config['private_key'] = "-----BEGIN PRIVATE KEY-----\n"
. chunk_split($config['private_key'], 64, "\n")
. "-----END PRIVATE KEY-----";
}
// 处理公钥
if (!empty($config['public_key']) && strpos($config['public_key'], '-----BEGIN') === false) {
$config['public_key'] = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split($config['public_key'], 64, "\n")
. "-----END PUBLIC KEY-----";
}
// 兼容merchant_public_key字段
if (empty($config['merchant_public_key'])) {
$config['merchant_public_key'] = $config['public_key'];
}
// 处理平台公钥
if (!empty($config['platform_public_key'])) {
// 如果有配置平台公钥且是BASE64格式添加PEM包装
if (strpos($config['platform_public_key'], '-----BEGIN') === false) {
$config['platform_public_key'] = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split($config['platform_public_key'], 64, "\n")
. "-----END PUBLIC KEY-----";
}
} else {
// 如果没有配置平台公钥,使用商户公钥作为默认值
$config['platform_public_key'] = $config['public_key'];
}
return $config;
}
/** /**
* 生成建行支付串 * 生成建行支付串
* 用于前端JSBridge调用建行收银台 * 用于前端JSBridge调用建行收银台
@ -155,9 +112,9 @@ class CcbPaymentService
$platformPubKey = $this->config['public_key']; // 服务方公钥 $platformPubKey = $this->config['public_key']; // 服务方公钥
$mac = strtoupper(md5($signString . '&PLATFORMPUB=' . $platformPubKey)); $mac = strtoupper(md5($signString . '&PLATFORMPUB=' . $platformPubKey));
// 使用RSA加密商户公钥后30位(用于ENCPUB字段) // ✅ 修复:使用 CcbRSA 加密商户公钥后30位用于ENCPUB字段
$encryption = new CcbEncryption($this->config); // 删除 CcbEncryption 类,统一使用 CcbRSA 处理密钥格式化
$encpub = $encryption->encryptMerchantPublicKeyLast30(); $encpub = $this->encryptPublicKeyLast30();
// 组装最终支付串 // 组装最终支付串
$finalPaymentString = $signString . '&MAC=' . $mac . '&PLATFORMID=' . $this->config['service_id'] . '&ENCPUB=' . urlencode($encpub); $finalPaymentString = $signString . '&MAC=' . $mac . '&PLATFORMID=' . $this->config['service_id'] . '&ENCPUB=' . urlencode($encpub);
@ -247,6 +204,43 @@ class CcbPaymentService
return implode(',', $orderItems); return implode(',', $orderItems);
} }
/**
* 加密商户公钥后30位用于支付串的ENCPUB字段
*
* 根据建行文档v2.2规范:
* "使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文"
*
* @return string BASE64编码的加密密文
* @throws \Exception
*/
private function encryptPublicKeyLast30()
{
$publicKey = $this->config['public_key'] ?? '';
if (empty($publicKey)) {
throw new \Exception('服务方公钥未配置');
}
// 去除 PEM 格式的头尾和空白字符,获取纯 BASE64 内容
$publicKeyContent = str_replace([
'-----BEGIN PUBLIC KEY-----',
'-----END PUBLIC KEY-----',
'-----BEGIN RSA PUBLIC KEY-----',
'-----END RSA PUBLIC KEY-----',
"\r", "\n", " ", "\t"
], '', $publicKey);
// 取后30位
$last30Chars = substr($publicKeyContent, -30);
if (strlen($last30Chars) < 30) {
throw new \Exception('商户公钥长度不足30位');
}
// ✅ 使用 CcbRSA 类进行加密(统一密钥格式化逻辑)
return CcbRSA::encrypt($last30Chars, $publicKey);
}
/** /**
* 处理支付回调 * 处理支付回调
* 建行支付完成后的同步回调 * 建行支付完成后的同步回调

View File

@ -140,9 +140,10 @@ class CcbRSA
return $publicKey; return $publicKey;
} }
// 格式化为PEM格式 // ✅ 修复: chunk_split()会在末尾添加换行符需要用rtrim()去除
// 否则会导致PEM格式中密钥内容和尾部之间有多余空行OpenSSL解析失败
$pem = "-----BEGIN PUBLIC KEY-----\n"; $pem = "-----BEGIN PUBLIC KEY-----\n";
$pem .= chunk_split($publicKey, 64, "\n"); $pem .= rtrim(chunk_split($publicKey, 64, "\n"), "\n") . "\n";
$pem .= "-----END PUBLIC KEY-----\n"; $pem .= "-----END PUBLIC KEY-----\n";
return $pem; return $pem;
@ -166,9 +167,10 @@ class CcbRSA
return $privateKey; return $privateKey;
} }
// 格式化为PEM格式 // ✅ 修复: chunk_split()会在末尾添加换行符需要用rtrim()去除
// 否则会导致PEM格式中密钥内容和尾部之间有多余空行OpenSSL解析失败
$pem = "-----BEGIN RSA PRIVATE KEY-----\n"; $pem = "-----BEGIN RSA PRIVATE KEY-----\n";
$pem .= chunk_split($privateKey, 64, "\n"); $pem .= rtrim(chunk_split($privateKey, 64, "\n"), "\n") . "\n";
$pem .= "-----END RSA PRIVATE KEY-----\n"; $pem .= "-----END RSA PRIVATE KEY-----\n";
return $pem; return $pem;