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.

217 lines
7.8 KiB

  1. <?php
  2. namespace App\Service\v3;
  3. use App\Exception\BusinessException;
  4. use Hyperf\Guzzle\ClientFactory;
  5. use Hyperf\Utils\ApplicationContext;
  6. /**
  7. * 建行支付相关接口
  8. */
  9. class CcbPay
  10. {
  11. private static ?CcbPay $_instance = null;
  12. private string $privateKey;
  13. private string $publicKey;
  14. private string $host = 'http://marketpayktwo.dev.jh:8028';
  15. private ClientFactory $clientFactory;
  16. public function __construct()
  17. {
  18. $this->privateKey = env('CCB_SELF_PRIVATE_KEY');
  19. $this->privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" . chunk_split($this->privateKey, 64, "\n") . "-----END RSA PRIVATE KEY-----\n";
  20. $this->publicKey = env('CCB_BANK_PUBLIC_KEY');
  21. $this->publicKey = "-----BEGIN PUBLIC KEY-----\n" . chunk_split($this->publicKey, 64, "\n") . "-----END PUBLIC KEY-----\n";
  22. $this->clientFactory = ApplicationContext::getContainer()->get(ClientFactory::class);
  23. }
  24. public static function getInstance(): CcbPay
  25. {
  26. if (self::$_instance === null) {
  27. self::$_instance = new self();
  28. }
  29. return self::$_instance;
  30. }
  31. /**
  32. * 发起退款
  33. */
  34. public function refundOrder(array $args): array
  35. {
  36. if (!isset($args['Ittparty_Jrnl_No'], $args['Py_Trn_No'])) {
  37. throw new BusinessException(500, '交易流水号、支付流水号不能为空');
  38. }
  39. $params = array_merge([
  40. 'Ittparty_Stm_Id' => '00000', //固定5个0
  41. 'Py_Chnl_Cd' => '0000000000000000000000000', // 因定25个0
  42. 'Ittparty_Tms' => date('YmdHis999'), // 时间yyyymmddhhmmssfff,年月日, 时分秒,毫秒
  43. 'Ittparty_Jrnl_No' => '', //该笔直连交易的客户方流水号(不允许重复)
  44. 'Mkt_Id' => env('CCB_MKT_ID'), //14位市场编号,该字段由银行在正式上线前提供,测试阶段有测试数据
  45. 'Py_Trn_No' => '', // 支付流水号,由建行生成,与该订单的支付动作唯一匹配
  46. // 'Rfnd_Amt' => 0, // 订单全额退款时不需要送,订单部分退款时必须送此值,且值等于所有子订单的退款金额之和
  47. /*'Sub_Ordr_List' => [ // 子订单列表,主订单全额退款时不需要传该域
  48. ],*/
  49. 'Vno' => '3', // 非必输
  50. ], $args);
  51. return $this->send('/online/direct/refundOrder', $params);
  52. }
  53. /**
  54. * 创建订单
  55. */
  56. public function gatherPlaceOrder(array $args): array
  57. {
  58. if (!isset($args['Ittparty_Jrnl_No'], $args['Main_Ordr_No'], $args['Ordr_Tamt'], $args['Txn_Tamt'], $args['Sub_Openid'], $args['Orderlist'])) {
  59. throw new BusinessException(500, '流水号、主订单流水号、付款金额、实付总金额、openid、Orderlist不能为空');
  60. }
  61. $params = array_merge([
  62. 'Ittparty_Stm_Id' => '00000', //固定5个0
  63. 'Py_Chnl_Cd' => '0000000000000000000000000', // 因定25个0
  64. 'Ittparty_Tms' => date('YmdHis888'), // 时间yyyymmddhhmmssfff,年月日, 时分秒,毫秒
  65. 'Ittparty_Jrnl_No' => '', //该笔直连交易的客户方流水号(不允许重复)
  66. 'Mkt_Id' => env('CCB_MKT_ID'), //14位市场编号,该字段由银行在正式上线前提供,测试阶段有测试数据
  67. 'Main_Ordr_No' => '', // 客户方主订单流水号,不允许重复
  68. 'Pymd_Cd' => env('CCB_PYMD_CD'), // 03 移动端H5页面 (app) 05 微信小程序(无收银台)
  69. 'Py_Ordr_Tpcd' => '04', //02 消费券购买订单 03 在途订单(只有是否支持在途模式为“是”时才可以使用)(注:品类管控市场订单类型必须为03) 04普通订单
  70. 'Py_Rslt_Ntc_Sn' => '1', // 支付结果通知给市场方维护的指定地址序号,对应建行后台设置的地址,1-10之间
  71. 'Ccy' => '156', // 156人民币
  72. 'Ordr_Tamt' => 0, // 应付总金额
  73. 'Txn_Tamt' => $args['Txn_Tamt'] ?? $args['Ordr_Tamt'], // 消费者实付总金额
  74. 'Sub_Appid' => env('APP_ID'), // “Pymd_Cd(支付方式代码)”为“05-微信小程序”时必输;当前调起支付的小程序APPID
  75. 'Sub_Openid' => '', // “Pymd_Cd(支付方式代码)”为“05-微信小程序”时必输;用户在小程序appid下的唯一标识
  76. 'Orderlist' => [
  77. 'Ordr_Amt' => $args['Ordr_Amt'] ?? $args['Ordr_Tamt'], // 订单商品总金额,即应付金额,所有商品订单金额之和等于主订单金额;
  78. 'Cmdty_Ordr_No' => '', // 返显输入接口中的客户方子订单编号
  79. 'Txnamt' => $args['Txnamt'] ?? $args['Ordr_Tamt'], // 消费者实付金额,所有商品订单金额之和等于主交易总金额金额
  80. 'Mkt_Mrch_Id' => '41060860800469061877', // 商家编号
  81. 'Clrg_Rule_Id' => 'F410608608004691879', // 分账规则编号,1.“Py_Ordr_Tpcd(订单类型)”为“02-消费券购买订单”时该字段无效,可不送;2.走默认分账策略,可不送;3.多个子订单时不可送
  82. 'Parlist' => [
  83. ['Seq_No' => '1', 'Mkt_Mrch_Id' => '41060860800469061878'], // Seq_No:参与方顺序号(默认从1开始);Mkt_Mrch_Id:商家编号
  84. ['Seq_No' => '2', 'Mkt_Mrch_Id' => '41060860800469000000'],
  85. ]
  86. ],
  87. 'Vno' => '4',
  88. ], $args);
  89. return $this->send('/online/direct/gatherPlaceorder', $params);
  90. }
  91. /**
  92. * 发送请求
  93. */
  94. private function send(string $url, array &$params): array
  95. {
  96. $this->SHA256WithRSASign($params); // 计算签名,加入签名参数
  97. $res = json_decode($this->clientFactory->create()->post($this->host . $url, ['json' => $params])->getBody()->getContents(), true);
  98. if (!$res || !isset($res['Svc_Rsp_St']) || $res['Svc_Rsp_St'] != '00') {
  99. throw new BusinessException(500, ($res['Rsp_Inf'] ?? '请求异常'));
  100. } else if (env('APP_ENV') != 'prod' && (!isset($res['Sign_Inf']) || !$this->SHA256WithRSAVerify($res))) {
  101. throw new BusinessException(500, '返回数据签名验证失败');
  102. }
  103. return $res;
  104. }
  105. /**
  106. * SHA256WithRSA加密
  107. */
  108. private function SHA256WithRSAEncrypt(string $data): string
  109. {
  110. openssl_private_encrypt(
  111. $data,
  112. $encrypted_data,
  113. openssl_get_privatekey($this->privateKey),
  114. );
  115. return base64_encode($encrypted_data);
  116. }
  117. /**
  118. * SHA256WithRSA解密
  119. */
  120. private function SHA256WithRSADecrypt(string $data)
  121. {
  122. openssl_public_decrypt(
  123. base64_decode($data),
  124. $decrypted_data,
  125. openssl_get_publickey($this->publicKey),
  126. OPENSSL_ALGO_SHA256
  127. );
  128. return $decrypted_data;
  129. }
  130. /**
  131. * SHA256WithRSA生成签名
  132. * @param array $params 请求的参数
  133. */
  134. private function SHA256WithRSASign(array &$params)
  135. {
  136. $this->kSort($params);
  137. openssl_sign(
  138. $this->joinStr($params),
  139. $binary_signature,
  140. openssl_get_privatekey($this->privateKey),
  141. OPENSSL_ALGO_SHA256,
  142. );
  143. $params['Sign_Inf'] = base64_encode($binary_signature);
  144. }
  145. /**
  146. * 字符串验签
  147. * openssl_verify => 1:签名正确;0:签名不正确;-1:验签出错error
  148. * @param array $params 请求接口后返回的数组
  149. * @return bool
  150. */
  151. public function SHA256WithRSAVerify(array $params): bool
  152. {
  153. // 公共参数不参与签名
  154. $sign = $params['Sign_Inf'];
  155. unset($params['Sign_Inf'], $params['Svc_Rsp_St'], $params['Svc_Rsp_Cd'], $params['Rsp_Inf']);
  156. $this->kSort($params);
  157. return openssl_verify(
  158. $this->joinStr($params),
  159. base64_decode($sign),
  160. openssl_get_publickey($this->publicKey),
  161. OPENSSL_ALGO_SHA256
  162. ) === 1;
  163. }
  164. /**
  165. * 数据按key排序,包括子元素
  166. */
  167. private function kSort(array &$params)
  168. {
  169. ksort($params);
  170. foreach ($params as &$item) {
  171. if (is_array($item)) {
  172. $this->kSort($item);
  173. }
  174. }
  175. }
  176. /**
  177. * 字符串拼接
  178. */
  179. private function joinStr(array $params): string
  180. {
  181. $str = '';
  182. foreach ($params as $key => $item) {
  183. if ($item === '' || $item === []) { // 如果参数的值为空则不参与签名
  184. continue;
  185. }
  186. if (is_array($item)) {
  187. $str .= (empty($str) ? '' : '&') . $this->joinStr($item);
  188. } else {
  189. $str .= (empty($str) ? '' : '&') . $key . '=' . $item;
  190. }
  191. }
  192. return $str;
  193. }
  194. }