You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

218 lines
7.8 KiB

<?php
namespace App\Service\v3;
use App\Exception\BusinessException;
use Hyperf\Guzzle\ClientFactory;
use Hyperf\Utils\ApplicationContext;
/**
* 建行支付相关接口
*/
class CcbPay
{
private static ?CcbPay $_instance = null;
private string $privateKey;
private string $publicKey;
private string $host = 'http://marketpayktwo.dev.jh:8028';
private ClientFactory $clientFactory;
public function __construct()
{
$this->privateKey = env('CCB_SELF_PRIVATE_KEY');
$this->privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" . chunk_split($this->privateKey, 64, "\n") . "-----END RSA PRIVATE KEY-----\n";
$this->publicKey = env('CCB_BANK_PUBLIC_KEY');
$this->publicKey = "-----BEGIN PUBLIC KEY-----\n" . chunk_split($this->publicKey, 64, "\n") . "-----END PUBLIC KEY-----\n";
$this->clientFactory = ApplicationContext::getContainer()->get(ClientFactory::class);
}
public static function getInstance(): CcbPay
{
if (self::$_instance === null) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* 发起退款
*/
public function refundOrder(array $args): array
{
if (!isset($args['Ittparty_Jrnl_No'], $args['Py_Trn_No'])) {
throw new BusinessException(500, '交易流水号、支付流水号不能为空');
}
$params = array_merge([
'Ittparty_Stm_Id' => '00000', //固定5个0
'Py_Chnl_Cd' => '0000000000000000000000000', // 因定25个0
'Ittparty_Tms' => date('YmdHis999'), // 时间yyyymmddhhmmssfff,年月日, 时分秒,毫秒
'Ittparty_Jrnl_No' => '', //该笔直连交易的客户方流水号(不允许重复)
'Mkt_Id' => env('CCB_MKT_ID'), //14位市场编号,该字段由银行在正式上线前提供,测试阶段有测试数据
'Py_Trn_No' => '', // 支付流水号,由建行生成,与该订单的支付动作唯一匹配
// 'Rfnd_Amt' => 0, // 订单全额退款时不需要送,订单部分退款时必须送此值,且值等于所有子订单的退款金额之和
/*'Sub_Ordr_List' => [ // 子订单列表,主订单全额退款时不需要传该域
],*/
'Vno' => '3', // 非必输
], $args);
return $this->send('/online/direct/refundOrder', $params);
}
/**
* 创建订单
*/
public function gatherPlaceOrder(array $args): array
{
if (!isset($args['Ittparty_Jrnl_No'], $args['Main_Ordr_No'], $args['Ordr_Tamt'], $args['Txn_Tamt'], $args['Sub_Openid'], $args['Orderlist'])) {
throw new BusinessException(500, '流水号、主订单流水号、付款金额、实付总金额、openid、Orderlist不能为空');
}
$params = array_merge([
'Ittparty_Stm_Id' => '00000', //固定5个0
'Py_Chnl_Cd' => '0000000000000000000000000', // 因定25个0
'Ittparty_Tms' => date('YmdHis888'), // 时间yyyymmddhhmmssfff,年月日, 时分秒,毫秒
'Ittparty_Jrnl_No' => '', //该笔直连交易的客户方流水号(不允许重复)
'Mkt_Id' => env('CCB_MKT_ID'), //14位市场编号,该字段由银行在正式上线前提供,测试阶段有测试数据
'Main_Ordr_No' => '', // 客户方主订单流水号,不允许重复
'Pymd_Cd' => env('CCB_PYMD_CD'), // 03 移动端H5页面 (app) 05 微信小程序(无收银台)
'Py_Ordr_Tpcd' => '04', //02 消费券购买订单 03 在途订单(只有是否支持在途模式为“是”时才可以使用)(注:品类管控市场订单类型必须为03) 04普通订单
'Py_Rslt_Ntc_Sn' => '1', // 支付结果通知给市场方维护的指定地址序号,对应建行后台设置的地址,1-10之间
'Ccy' => '156', // 156人民币
'Ordr_Tamt' => 0, // 应付总金额
'Txn_Tamt' => $args['Txn_Tamt'] ?? $args['Ordr_Tamt'], // 消费者实付总金额
'Sub_Appid' => env('APP_ID'), // “Pymd_Cd(支付方式代码)”为“05-微信小程序”时必输;当前调起支付的小程序APPID
'Sub_Openid' => '', // “Pymd_Cd(支付方式代码)”为“05-微信小程序”时必输;用户在小程序appid下的唯一标识
'Orderlist' => [
'Ordr_Amt' => $args['Ordr_Amt'] ?? $args['Ordr_Tamt'], // 订单商品总金额,即应付金额,所有商品订单金额之和等于主订单金额;
'Cmdty_Ordr_No' => '', // 返显输入接口中的客户方子订单编号
'Txnamt' => $args['Txnamt'] ?? $args['Ordr_Tamt'], // 消费者实付金额,所有商品订单金额之和等于主交易总金额金额
'Mkt_Mrch_Id' => '41060860800469061877', // 商家编号
'Clrg_Rule_Id' => 'F410608608004691879', // 分账规则编号,1.“Py_Ordr_Tpcd(订单类型)”为“02-消费券购买订单”时该字段无效,可不送;2.走默认分账策略,可不送;3.多个子订单时不可送
'Parlist' => [
['Seq_No' => '1', 'Mkt_Mrch_Id' => '41060860800469061878'], // Seq_No:参与方顺序号(默认从1开始);Mkt_Mrch_Id:商家编号
['Seq_No' => '2', 'Mkt_Mrch_Id' => '41060860800469000000'],
]
],
'Vno' => '4',
], $args);
return $this->send('/online/direct/gatherPlaceorder', $params);
}
/**
* 发送请求
*/
private function send(string $url, array &$params): array
{
$this->SHA256WithRSASign($params); // 计算签名,加入签名参数
$res = json_decode($this->clientFactory->create()->post($this->host . $url, ['json' => $params])->getBody()->getContents(), true);
if (!$res || !isset($res['Svc_Rsp_St']) || $res['Svc_Rsp_St'] != '00') {
throw new BusinessException(500, ($res['Rsp_Inf'] ?? '请求异常'));
} else if (env('APP_ENV') != 'prod' && (!isset($res['Sign_Inf']) || !$this->SHA256WithRSAVerify($res))) {
throw new BusinessException(500, '返回数据签名验证失败');
}
return $res;
}
/**
* SHA256WithRSA加密
*/
private function SHA256WithRSAEncrypt(string $data): string
{
openssl_private_encrypt(
$data,
$encrypted_data,
openssl_get_privatekey($this->privateKey),
);
return base64_encode($encrypted_data);
}
/**
* SHA256WithRSA解密
*/
private function SHA256WithRSADecrypt(string $data)
{
openssl_public_decrypt(
base64_decode($data),
$decrypted_data,
openssl_get_publickey($this->publicKey),
OPENSSL_ALGO_SHA256
);
return $decrypted_data;
}
/**
* SHA256WithRSA生成签名
* @param array $params 请求的参数
*/
private function SHA256WithRSASign(array &$params)
{
$this->kSort($params);
openssl_sign(
$this->joinStr($params),
$binary_signature,
openssl_get_privatekey($this->privateKey),
OPENSSL_ALGO_SHA256,
);
$params['Sign_Inf'] = base64_encode($binary_signature);
}
/**
* 字符串验签
* openssl_verify => 1:签名正确;0:签名不正确;-1:验签出错error
* @param array $params 请求接口后返回的数组
* @return bool
*/
public function SHA256WithRSAVerify(array $params): bool
{
// 公共参数不参与签名
$sign = $params['Sign_Inf'];
unset($params['Sign_Inf'], $params['Svc_Rsp_St'], $params['Svc_Rsp_Cd'], $params['Rsp_Inf']);
$this->kSort($params);
return openssl_verify(
$this->joinStr($params),
base64_decode($sign),
openssl_get_publickey($this->publicKey),
OPENSSL_ALGO_SHA256
) === 1;
}
/**
* 数据按key排序,包括子元素
*/
private function kSort(array &$params)
{
ksort($params);
foreach ($params as &$item) {
if (is_array($item)) {
$this->kSort($item);
}
}
}
/**
* 字符串拼接
*/
private function joinStr(array $params): string
{
$str = '';
foreach ($params as $key => $item) {
if ($item === '' || $item === []) { // 如果参数的值为空则不参与签名
continue;
}
if (is_array($item)) {
$str .= (empty($str) ? '' : '&') . $this->joinStr($item);
} else {
$str .= (empty($str) ? '' : '&') . $key . '=' . $item;
}
}
return $str;
}
}