Browse Source

建行支付test

ccb_pay2
yangrz 4 years ago
parent
commit
58ac38f9c0
  1. 34
      app/Controller/v3/CCBNotifyController.php
  2. 441
      app/Service/v3/CCBPayment.php
  3. 6
      config/config.php
  4. 10
      config/routes.php
  5. 88
      test/Cases/CCBTest.php

34
app/Controller/v3/CCBNotifyController.php

@ -0,0 +1,34 @@
<?php
class CCBNotifyController extends BaseController
{
public function pay()
{
}
public function refund()
{
}
public function merchant()
{
}
public function accounting()
{
}
public function bill()
{
}
public function platform()
{
}
}

441
app/Service/v3/CCBPayment.php

@ -0,0 +1,441 @@
<?php
namespace App\Service\v3;
use App\Exception\BusinessException;
use Hyperf\Guzzle\ClientFactory;
use Hyperf\Logger\LoggerFactory;
class CCBPayment
{
/**
* 是否测试环境
* @var bool
*/
private $isDebug;
/**
* 测试环境请求的域名
* @var string
*/
private $devBaseUri = 'http://marketpayktwo.dev.jh:8028';
/**
* 生产环境请求的域名
* @var string
*/
private $prodBaseUri = 'https://marketpay.ccb.com';
/**
* 市场编号
* @var string
*/
private $mktId;
/**
* 我方私钥
* @var resource
*/
private $selfPrivateKey;
/**
* 银行公钥
* @var resource
*/
private $bankPublicKey;
/**
* 发起渠道编号
* @var string
*/
private $ittpartyStmId = '00000';
/**
* 支付渠道代码
* @var string
*/
private $pyChnlCd = '0000000000000000000000000';
/**
* @var \Hyperf\Guzzle\ClientFactory
*/
private $clientFactory;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
public function __construct(ClientFactory $clientFactory, LoggerFactory $loggerFactory)
{
$this->clientFactory = $clientFactory;
$this->logger = $loggerFactory->get('ccb');
$this->isDebug = config('ccb.debug');
$this->mktId = config('ccb.mkt_id');
$selfPrivateKey = config('ccb.self_private_key');
$selfPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split($selfPrivateKey, 64, "\n")."-----END RSA PRIVATE KEY-----\n";
$this->selfPrivateKey = openssl_get_privatekey($selfPrivateKey);
$bankPublicKey = config('ccb.bank_public_key');
$bankPublicKey = "-----BEGIN PUBLIC KEY-----\n".chunk_split($bankPublicKey, 64, "\n")."-----END PUBLIC KEY-----\n";
$this->bankPublicKey = openssl_get_publickey($bankPublicKey);
}
/**
* 使用我方私钥加密
* @param string $data
* @return string
*/
public function encrypt(string $data)
{
$str = '';
foreach (str_split($data, 117) as $chunk) {
openssl_private_encrypt($chunk, $crypted, $this->selfPrivateKey);
$str .= $crypted;
}
return base64_encode($str);
}
/**
* 使用银行公钥解密
* @param string $data
* @return string
*/
public function decrypt(string $data)
{
$raw = base64_decode($data);
$str = '';
foreach (str_split($raw, 256) as $chunk) {
openssl_public_decrypt($chunk, $decrypted, $this->bankPublicKey);
$str .= $decrypted;
}
return $str;
}
/**
* 使用我方私钥生成签名
* @param string $data
* @return string
*/
public function sign(string $data)
{
openssl_sign($data, $signature, $this->selfPrivateKey, OPENSSL_ALGO_SHA256);
return base64_encode($signature);
}
/**
* 使用银行公钥验证签名
* @param string $data
* @param string $signature
* @return bool
*/
public function verifySign(string $data, string $signature)
{
$signature = base64_decode($signature);
$result = openssl_verify($data, $signature, $this->bankPublicKey, OPENSSL_ALGO_SHA256);
return $result == 1;
}
/**
* 计算待签名字符串
* @param array $params
* @return string
*/
public function createSign(array $params)
{
// 不参与签名的字符串
$unsignKeys = ['Sign_Inf', 'Svc_Rsp_St', 'Svc_Rsp_Cd', 'Rsp_Inf'];
$result = [];
ksort($params);
foreach ($params as $key => $item) {
if (in_array($key, $unsignKeys)) {
continue;
}
if (is_array($item)) {
foreach ($item as $child) {
$value = $this->createSign($child);
$result[] = ['', $value];
}
} else {
$value = trim($item);
if ($value !== '') {
$result[] = [$key, $value];
}
}
}
$str = '';
foreach ($result as [$key, $value]) {
if ($value) {
$str .= $key ? "$key=$value&" : "$value&";
}
}
return rtrim($str, '&');
}
/**
* 发送API请求
* @param string $uri
* @param array $params
* @return array
*/
private function apiRequest(string $uri, array $params = []): array
{
$signData = $this->createSign($params);
$params['Sign_Inf'] = $this->sign($signData);
if ($this->isDebug) {
$uri = $this->devBaseUri.$uri;
} else {
$uri = $this->prodBaseUri.$uri;
}
$options = ['json' => $params];
if (env('CCB_HTTP_PROXY')) {
$options['proxy'] = env('CCB_HTTP_PROXY');
}
try {
$response = $this->clientFactory->create(['timeout' => 120])->post($uri, $options);
} catch (\Exception $e) {
$this->saveApiLog($uri, $params, $e->getMessage());
throw new BusinessException(500, '请求异常');
}
$content = $response->getBody()->getContents();
$result = json_decode($content, true);
if (!isset($result['Svc_Rsp_St']) || $result['Svc_Rsp_St'] != '00') {
$this->saveApiLog($uri, $params, $content);
throw new BusinessException(500, ($result['Rsp_Inf'] ?? 'CCB请求失败'));
}
if (!isset($result['Sign_Inf']) || !$this->verifySign($this->createSign($result), $result['Sign_Inf'])) {
$this->saveApiLog($uri, $params, $content);
throw new BusinessException(500, ($result['Rsp_Inf'] ?? 'CCB验签失败'));
}
if ($this->isDebug) {
$this->saveApiLog($uri, $params, $content);
}
return $result;
}
/**
* 保存API请求日志
* @param $uri
* @param $params
* @param $content
* @return void
*/
private function saveApiLog($uri, $params, $content)
{
$this->logger->info(
sprintf(
"%s\nUrl:%s\nParams:\n%s\nContent:\n%s\n",
'CCB API Log',
$uri,
json_encode($params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
$content
)
);
}
private function getTimestamp()
{
return (new \DateTime())->format('YmdHisv');
}
private function genSerialNumber()
{
return date('YmdHis').mt_rand(10000, 99999).mt_rand(10000, 99999);
}
/**
* 3.1 生成支付订单接口
* @param string $Main_Ordr_No 主订单编号
* @param string $Ordr_Tamt 订单总金额
* @param string $Txn_Tamt 交易总金额(实付)
* @param string $Pymd_Cd 支付方式:03为H5,05为小程序
* @param string $Sub_Appid 当前调起支付的小程序appid
* @param string $Sub_Openid 用户在小程序下的openid
* @param array $Orderlist 子订单列表
* @return array
*/
public function gatherPlaceorder(string $Main_Ordr_No, string $Ordr_Tamt, string $Txn_Tamt, string $Pymd_Cd, string $Sub_Appid, string $Sub_Openid, array $Orderlist)
{
$uri = '/online/direct/gatherPlaceorder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Main_Ordr_No' => $Main_Ordr_No,
'Pymd_Cd' => $Pymd_Cd,
'Py_Ordr_Tpcd' => '04',
'Py_Rslt_Ntc_Sn' => '1',
'Ccy' => '156',
'Ordr_Tamt' => $Ordr_Tamt,
'Txn_Tamt' => $Txn_Tamt,
'Sub_Appid' => $Sub_Appid,
'Sub_Openid' => $Sub_Openid,
'Order_Time_Out' => '1800',
'Orderlist' => $Orderlist,
'Vno' => '4',
];
return $this->apiRequest($uri, $params);
}
/**
* 下单子订单格式化
* @param string $Mkt_Mrch_Id 商家编号
* @param string $Cmdty_Ordr_No 子订单编号
* @param string $Ordr_Amt 订单金额
* @param string $Txnamt 实付金额
* @param string $Clrg_Rule_Id 分账规则编号
* @param array $Parlist 分账参与方列表
* @return string[]
*/
public function subOrderListItem(string $Mkt_Mrch_Id, string $Cmdty_Ordr_No, string $Ordr_Amt, string $Txnamt, string $Clrg_Rule_Id, array $Parlist)
{
return compact('Mkt_Mrch_Id', 'Cmdty_Ordr_No', 'Ordr_Amt', 'Txnamt', 'Clrg_Rule_Id', 'Parlist');
}
/**
* 分账参与方
* @param int $Seq_No 顺序号
* @param string $Mkt_Mrch_Id 商家编号
* @return array
*/
public function parListItem(int $Seq_No, string $Mkt_Mrch_Id)
{
return compact('Seq_No', 'Mkt_Mrch_Id');
}
/**
* 3.4 查询支付结果接口,同一笔订单不支持并发查询
* @param string $Main_Ordr_No 主订单号,主订单号与支付流水号必输其一
* @param string $Py_Trn_No 银行支付流水号
* @return array
*/
public function gatherEnquireOrder(string $Main_Ordr_No, string $Py_Trn_No = '')
{
$uri = '/online/direct/gatherEnquireOrder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Main_Ordr_No' => $Main_Ordr_No,
'Py_Trn_No' => $Py_Trn_No,
'Vno' => '4',
];
return $this->apiRequest($uri, $params);
}
/**
* 4.1 订单退款接口
* @param string $Cust_Rfnd_Trcno 退款流水号,对于同一笔退款唯一
* @param string $Py_Trn_No 银行支付流水号
* @param string $Rfnd_Amt 退款金额,全额退款时可不传
* @param array $Sub_Ordr_List 子订单列表,全额退款时不需要传该域
* @return array
*/
public function refundOrder(string $Cust_Rfnd_Trcno, string $Py_Trn_No, string $Rfnd_Amt = '', array $Sub_Ordr_List = [])
{
$uri = '/online/direct/refundOrder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
'Py_Trn_No' => $Py_Trn_No,
'Rfnd_Amt' => $Rfnd_Amt,
'Sub_Ordr_List' => $Sub_Ordr_List,
'Vno' => '3',
];
if ($Sub_Ordr_List) {
$params['Sub_Ordr_List'] = $Sub_Ordr_List;
}
return $this->apiRequest($uri, $params);
}
/**
* 退款子订单格式化
* @param string $Sub_Ordr_Id 银行子订单编号
* @param string $Rfnd_Amt 退款金额
* @return string[]
*/
public function refundSubOrderListItem(string $Sub_Ordr_Id, string $Rfnd_Amt)
{
return compact('Sub_Ordr_Id', 'Rfnd_Amt');
}
/**
* 4.3 查询退款结果接口,同一笔退款不支持并发查询
* @param string $Cust_Rfnd_Trcno 我方退款流水号,与银行退款流水号必输其一
* @param string $Rfnd_Trcno 银行退款流水号
* @return array
*/
public function enquireRefundOrder(string $Cust_Rfnd_Trcno, string $Rfnd_Trcno = '')
{
$uri = '/online/direct/enquireRefundOrder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
'Rfnd_Trcno' => $Rfnd_Trcno,
'Vno' => '4',
];
return $this->apiRequest($uri, $params);
}
/**
* 5.3 确认收货接口
* @param string $Prim_Ordr_No 银行主订单编号
* @return array
*/
public function mergeNoticeArrival(string $Prim_Ordr_No)
{
$uri = '/online/direct/enquireRefundOrder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Prim_Ordr_No' => $Prim_Ordr_No,
'Vno' => '4',
];
return $this->apiRequest($uri, $params);
}
}

6
config/config.php

@ -64,4 +64,10 @@ return [
'distance' => [
'delivery_distance' => env('DELIVERY_DISTANCE', '7000')
],
'ccb' => [
'debug' => env('CCB_DEBUG', false),
'mkt_id' => env('CCB_MKT_ID', ''),
'self_private_key' => env('CCB_SELF_PRIVATE_KEY', ''),
'bank_public_key' => env('CCB_BANK_PUBLIC_KEY', ''),
],
];

10
config/routes.php

@ -186,4 +186,14 @@ Router::addGroup('/v3/wechat/',function () {
Router::post('notify/online', 'App\Controller\v3\NotifyController@wxminiOnline');
Router::post('notify/offline', 'App\Controller\v3\NotifyController@wxminiOffline');
Router::post('notify/refund', 'App\Controller\v3\NotifyController@wxminiRefund');
});
// 建行支付回调
Router::addGroup('/v3/ccb/',function () {
Router::post('notify/pay', 'App\Controller\v3\CCBNotifyController@pay');
Router::post('notify/refund', 'App\Controller\v3\NotifyController@refund');
Router::post('notify/merchant', 'App\Controller\v3\NotifyController@merchant');
Router::post('notify/accounting', 'App\Controller\v3\NotifyController@accounting');
Router::post('notify/bill', 'App\Controller\v3\NotifyController@bill');
Router::post('notify/platform', 'App\Controller\v3\NotifyController@platform');
});

88
test/Cases/CCBTest.php

@ -0,0 +1,88 @@
<?php
namespace HyperfTest\Cases;
use App\Service\v3\CCBPayment;
use Hyperf\Utils\ApplicationContext;
use HyperfTest\HttpTestCase;
class CCBTest extends HttpTestCase
{
public function testSign()
{
$ccb = ApplicationContext::getContainer()->get(CCBPayment::class);
$data = "Write Once, Run Anywhere";
$result = $ccb->sign($data);
$s = 'odUeD1V6obC/j8lUvmFwn6LSQ2DrvaDEin5DKs0FiB/HttboPJncmLisH22Y1grPezE0a+Ij6cdd5Taof8e4A76pKdXA+hDGz0nPMlCRgSF5tHQ6uVXktL/3lqpVTX6ECjRoHmzmo6cAMqVXYQKEl56r4gJcBPW4X4ghRtTgw9AK8+8b8O5EAqxuPxMyhSOYrCsUYRXapezV3uioEReYjvhg/u+kRf662P2nL0ab3szGMXMAoE+JjgMAHR9WCL3Can/5ADgoFff/7kMSxrA7/r94EfkDN50IvRVAx9WBLx3+WWcRRfM6JcqZ91B+g00wYN/OSZHTJDPgV6Ofd5cBpA==';
$this->assertTrue($result == $s);
}
public function testCreateSign()
{
$ccb = ApplicationContext::getContainer()->get(CCBPayment::class);
$params = json_decode('{"Blank3":" ","Blank2":"","Sign_Inf":"signInf","Rsp_Inf":"rspInf","Svc_Rsp_St":"svcRspSt","Svc_Rsp_Cd":"svcRspCd","Amt":"amt","Pymd_Cd":"pymdCd","Parlist":[{"Seq_No":"seqNo","Mkt_Mrch_Id":"mktMrchIdFj"},{"Seq_No":"seqNo","Xbb":[{"sdc":"1131","xyz":"xxxx"}],"Mkt_Mrch_Id":"mktMrchIdFj2"},{"Seq_No":"seqNo3","Mkt_Mrch_Id":"mktMrchIdFj3"}]}', true);
$str = $ccb->createSign($params);
$s = 'Amt=amt&Mkt_Mrch_Id=mktMrchIdFj&Seq_No=seqNo&Mkt_Mrch_Id=mktMrchIdFj2&Seq_No=seqNo&sdc=1131&xyz=xxxx&Mkt_Mrch_Id=mktMrchIdFj3&Seq_No=seqNo3&Pymd_Cd=pymdCd';
$this->assertTrue($str == $s);
}
public function testOrder()
{
$ccb = ApplicationContext::getContainer()->get(CCBPayment::class);
$parList = [];
$parList[] = $ccb->parListItem(1, '41060860800469000000');
$parList[] = $ccb->parListItem(2, '41060860800469061877');
$subOrderList[] = $ccb->subOrderListItem(
'41060860800469061877',
'151152',
'1.23',
'1.23',
'F410608608004691879',
$parList
);
$parList = [];
$parList[] = $ccb->parListItem(1, '41060860800469000000');
$parList[] = $ccb->parListItem(2, '41060860800469061878');
$subOrderList[] = $ccb->subOrderListItem(
'41060860800469061878',
'151153',
'1.00',
'1.00',
'F410608608004691879',
$parList
);
$result = $ccb->gatherPlaceorder(
'c2020060915410278957',
'2.23',
'2.23',
'03',
'',
'',
$subOrderList
);
var_export($result);
}
public function testQuery()
{
$ccb = ApplicationContext::getContainer()->get(CCBPayment::class);
$result = $ccb->gatherEnquireOrder('c2020060915410278956');
var_dump($result);
}
}
Loading…
Cancel
Save