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