5 changed files with 579 additions and 0 deletions
-
34app/Controller/v3/CCBNotifyController.php
-
441app/Service/v3/CCBPayment.php
-
6config/config.php
-
10config/routes.php
-
88test/Cases/CCBTest.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() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
|
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue