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
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;
|
|
}
|
|
}
|