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