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.

429 lines
13 KiB

  1. <?php
  2. namespace App\Service\v3;
  3. use App\Exception\BusinessException;
  4. use Hyperf\Guzzle\ClientFactory;
  5. use Hyperf\Logger\LoggerFactory;
  6. class CCBPayment
  7. {
  8. /**
  9. * 是否测试环境
  10. * @var bool
  11. */
  12. private $isDebug;
  13. /**
  14. * 测试环境请求的域名
  15. * @var string
  16. */
  17. private $devBaseUri = 'http://marketpayktwo.dev.jh:8028';
  18. /**
  19. * 生产环境请求的域名
  20. * @var string
  21. */
  22. private $prodBaseUri = 'https://marketpay.ccb.com';
  23. /**
  24. * 市场编号
  25. * @var string
  26. */
  27. private $mktId;
  28. /**
  29. * 我方私钥
  30. * @var resource
  31. */
  32. private $selfPrivateKey;
  33. /**
  34. * 银行公钥
  35. * @var resource
  36. */
  37. private $bankPublicKey;
  38. /**
  39. * 发起渠道编号
  40. * @var string
  41. */
  42. private $ittpartyStmId = '00000';
  43. /**
  44. * 支付渠道代码
  45. * @var string
  46. */
  47. private $pyChnlCd = '0000000000000000000000000';
  48. /**
  49. * @var \Hyperf\Guzzle\ClientFactory
  50. */
  51. private $clientFactory;
  52. /**
  53. * @var \Psr\Log\LoggerInterface
  54. */
  55. private $logger;
  56. public function __construct(ClientFactory $clientFactory, LoggerFactory $loggerFactory)
  57. {
  58. $this->clientFactory = $clientFactory;
  59. $this->logger = $loggerFactory->get('ccb');
  60. $this->isDebug = config('ccb.debug');
  61. $this->mktId = config('ccb.mkt_id');
  62. $selfPrivateKey = config('ccb.self_private_key');
  63. $selfPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split($selfPrivateKey, 64, "\n")."-----END RSA PRIVATE KEY-----\n";
  64. $this->selfPrivateKey = openssl_get_privatekey($selfPrivateKey);
  65. $bankPublicKey = config('ccb.bank_public_key');
  66. $bankPublicKey = "-----BEGIN PUBLIC KEY-----\n".chunk_split($bankPublicKey, 64, "\n")."-----END PUBLIC KEY-----\n";
  67. $this->bankPublicKey = openssl_get_publickey($bankPublicKey);
  68. }
  69. /**
  70. * 使用我方私钥加密
  71. * @param string $data
  72. * @return string
  73. */
  74. public function encrypt(string $data)
  75. {
  76. $str = '';
  77. foreach (str_split($data, 117) as $chunk) {
  78. openssl_private_encrypt($chunk, $crypted, $this->selfPrivateKey);
  79. $str .= $crypted;
  80. }
  81. return base64_encode($str);
  82. }
  83. /**
  84. * 使用银行公钥解密
  85. * @param string $data
  86. * @return string
  87. */
  88. public function decrypt(string $data)
  89. {
  90. $raw = base64_decode($data);
  91. $str = '';
  92. foreach (str_split($raw, 256) as $chunk) {
  93. openssl_public_decrypt($chunk, $decrypted, $this->bankPublicKey);
  94. $str .= $decrypted;
  95. }
  96. return $str;
  97. }
  98. /**
  99. * 使用我方私钥生成签名
  100. * @param string $data
  101. * @return string
  102. */
  103. public function sign(string $data)
  104. {
  105. openssl_sign($data, $signature, $this->selfPrivateKey, OPENSSL_ALGO_SHA256);
  106. return base64_encode($signature);
  107. }
  108. /**
  109. * 使用银行公钥验证签名
  110. * @param string $data
  111. * @param string $signature
  112. * @return bool
  113. */
  114. public function verifySign(string $data, string $signature)
  115. {
  116. $signature = base64_decode($signature);
  117. $result = openssl_verify($data, $signature, $this->bankPublicKey, OPENSSL_ALGO_SHA256);
  118. return $result == 1;
  119. }
  120. /**
  121. * 计算待签名字符串
  122. * @param array $params
  123. * @return string
  124. */
  125. public function createSign(array $params)
  126. {
  127. // 不参与签名的字符串
  128. $unsignKeys = ['Sign_Inf', 'Svc_Rsp_St', 'Svc_Rsp_Cd', 'Rsp_Inf'];
  129. $result = [];
  130. ksort($params);
  131. foreach ($params as $key => $item) {
  132. if (in_array($key, $unsignKeys)) {
  133. continue;
  134. }
  135. if (is_array($item)) {
  136. foreach ($item as $child) {
  137. $value = $this->createSign($child);
  138. $result[] = ['', $value];
  139. }
  140. } else {
  141. $value = trim($item);
  142. if ($value !== '') {
  143. $result[] = [$key, $value];
  144. }
  145. }
  146. }
  147. $str = '';
  148. foreach ($result as [$key, $value]) {
  149. if ($value) {
  150. $str .= $key ? "$key=$value&" : "$value&";
  151. }
  152. }
  153. return rtrim($str, '&');
  154. }
  155. /**
  156. * 发送API请求
  157. * @param string $uri
  158. * @param array $params
  159. * @return array
  160. */
  161. private function apiRequest(string $uri, array $params = []): array
  162. {
  163. $signData = $this->createSign($params);
  164. $params['Sign_Inf'] = $this->sign($signData);
  165. if ($this->isDebug) {
  166. $uri = $this->devBaseUri.$uri;
  167. } else {
  168. $uri = $this->prodBaseUri.$uri;
  169. }
  170. $options = ['json' => $params];
  171. $startTime = microtime(true);
  172. try {
  173. $response = $this->clientFactory->create(['timeout' => 60])->post($uri, $options);
  174. } catch (\Exception $e) {
  175. $this->saveApiLog(0, $uri, $params, $e->getMessage());
  176. throw new BusinessException(500, '请求异常');
  177. }
  178. $useTime = round((microtime(true) - $startTime) * 1000, 2);
  179. $content = $response->getBody()->getContents();
  180. $result = json_decode($content, true);
  181. if (!isset($result['Svc_Rsp_St']) || $result['Svc_Rsp_St'] != '00') {
  182. $this->saveApiLog($useTime, $uri, $params, $content);
  183. throw new BusinessException(500, ($result['Rsp_Inf'] ?? 'CCB请求失败'));
  184. }
  185. if (!isset($result['Sign_Inf']) || !$this->verifySign($this->createSign($result), $result['Sign_Inf'])) {
  186. $this->saveApiLog($useTime, $uri, $params, $content);
  187. throw new BusinessException(500, ($result['Rsp_Inf'] ?? 'CCB验签失败'));
  188. }
  189. if ($this->isDebug) {
  190. $this->saveApiLog($useTime, $uri, $params, $content);
  191. }
  192. return $result;
  193. }
  194. /**
  195. * 保存API请求日志
  196. * @param $useTime
  197. * @param $uri
  198. * @param $params
  199. * @param $content
  200. * @return void
  201. */
  202. private function saveApiLog($useTime, $uri, $params, $content)
  203. {
  204. $this->logger->info(
  205. sprintf(
  206. "%s\nTime: %.2f ms\nUrl: %s\nParams:\n%s\nContent:\n%s\n",
  207. 'CCB API Log',
  208. $useTime,
  209. $uri,
  210. json_encode($params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
  211. $content
  212. )
  213. );
  214. }
  215. private function getTimestamp()
  216. {
  217. return (new \DateTime())->format('YmdHisv');
  218. }
  219. private function genSerialNumber()
  220. {
  221. return date('YmdHis').mt_rand(10000, 99999).mt_rand(10000, 99999);
  222. }
  223. /**
  224. * 3.1 生成支付订单接口
  225. * @param string $Main_Ordr_No 主订单编号
  226. * @param string $Ordr_Tamt 订单总金额
  227. * @param string $Txn_Tamt 交易总金额(实付)
  228. * @param string $Pymd_Cd 支付方式:03为H5,05为小程序
  229. * @param string $Sub_Appid 当前调起支付的小程序appid
  230. * @param string $Sub_Openid 用户在小程序下的openid
  231. * @param array $Orderlist 子订单列表
  232. * @return array
  233. */
  234. public function gatherPlaceorder(string $Main_Ordr_No, string $Ordr_Tamt, string $Txn_Tamt, string $Pymd_Cd, string $Sub_Appid, string $Sub_Openid, array $Orderlist)
  235. {
  236. $uri = '/online/direct/gatherPlaceorder';
  237. $params = [
  238. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  239. 'Py_Chnl_Cd' => $this->pyChnlCd,
  240. 'Ittparty_Tms' => $this->getTimestamp(),
  241. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  242. 'Mkt_Id' => $this->mktId,
  243. 'Main_Ordr_No' => $Main_Ordr_No,
  244. 'Pymd_Cd' => $Pymd_Cd,
  245. 'Py_Ordr_Tpcd' => '04',
  246. 'Py_Rslt_Ntc_Sn' => '1',
  247. 'Ccy' => '156',
  248. 'Ordr_Tamt' => $Ordr_Tamt,
  249. 'Txn_Tamt' => $Txn_Tamt,
  250. 'Sub_Appid' => $Sub_Appid,
  251. 'Sub_Openid' => $Sub_Openid,
  252. 'Order_Time_Out' => '1800',
  253. 'Orderlist' => $Orderlist,
  254. 'Vno' => '4',
  255. ];
  256. return $this->apiRequest($uri, $params);
  257. }
  258. /**
  259. * 下单子订单格式化
  260. * @param string $Mkt_Mrch_Id 商家编号
  261. * @param string $Cmdty_Ordr_No 子订单编号
  262. * @param string $Ordr_Amt 订单金额
  263. * @param string $Txnamt 实付金额
  264. * @return string[]
  265. */
  266. public function subOrderListItem(string $Mkt_Mrch_Id, string $Cmdty_Ordr_No, string $Ordr_Amt, string $Txnamt)
  267. {
  268. return compact('Mkt_Mrch_Id', 'Cmdty_Ordr_No', 'Ordr_Amt', 'Txnamt');
  269. }
  270. /**
  271. * 3.4 查询支付结果接口,同一笔订单不支持并发查询
  272. * @param string $Main_Ordr_No 主订单号,主订单号与支付流水号必输其一
  273. * @param string $Py_Trn_No 银行支付流水号
  274. * @return array
  275. */
  276. public function gatherEnquireOrder(string $Main_Ordr_No, string $Py_Trn_No = '')
  277. {
  278. $uri = '/online/direct/gatherEnquireOrder';
  279. $params = [
  280. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  281. 'Py_Chnl_Cd' => $this->pyChnlCd,
  282. 'Ittparty_Tms' => $this->getTimestamp(),
  283. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  284. 'Mkt_Id' => $this->mktId,
  285. 'Main_Ordr_No' => $Main_Ordr_No,
  286. 'Py_Trn_No' => $Py_Trn_No,
  287. 'Vno' => '4',
  288. ];
  289. return $this->apiRequest($uri, $params);
  290. }
  291. /**
  292. * 4.1 订单退款接口
  293. * @param string $Cust_Rfnd_Trcno 退款流水号,对于同一笔退款唯一
  294. * @param string $Py_Trn_No 银行支付流水号
  295. * @param string $Rfnd_Amt 退款金额,全额退款时可不传
  296. * @param array $Sub_Ordr_List 子订单列表,全额退款时不需要传该域
  297. * @return array
  298. */
  299. public function refundOrder(string $Cust_Rfnd_Trcno, string $Py_Trn_No, string $Rfnd_Amt = '', array $Sub_Ordr_List = [])
  300. {
  301. $uri = '/online/direct/refundOrder';
  302. $params = [
  303. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  304. 'Py_Chnl_Cd' => $this->pyChnlCd,
  305. 'Ittparty_Tms' => $this->getTimestamp(),
  306. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  307. 'Mkt_Id' => $this->mktId,
  308. 'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
  309. 'Py_Trn_No' => $Py_Trn_No,
  310. 'Rfnd_Amt' => $Rfnd_Amt,
  311. 'Sub_Ordr_List' => $Sub_Ordr_List,
  312. 'Vno' => '3',
  313. ];
  314. if ($Sub_Ordr_List) {
  315. $params['Sub_Ordr_List'] = $Sub_Ordr_List;
  316. }
  317. return $this->apiRequest($uri, $params);
  318. }
  319. /**
  320. * 退款子订单格式化
  321. * @param string $Sub_Ordr_Id 银行子订单编号
  322. * @param string $Rfnd_Amt 退款金额
  323. * @return string[]
  324. */
  325. public function refundSubOrderListItem(string $Sub_Ordr_Id, string $Rfnd_Amt)
  326. {
  327. return compact('Sub_Ordr_Id', 'Rfnd_Amt');
  328. }
  329. /**
  330. * 4.3 查询退款结果接口,同一笔退款不支持并发查询
  331. * @param string $Cust_Rfnd_Trcno 我方退款流水号,与银行退款流水号必输其一
  332. * @param string $Rfnd_Trcno 银行退款流水号
  333. * @return array
  334. */
  335. public function enquireRefundOrder(string $Cust_Rfnd_Trcno, string $Rfnd_Trcno = '')
  336. {
  337. $uri = '/online/direct/enquireRefundOrder';
  338. $params = [
  339. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  340. 'Py_Chnl_Cd' => $this->pyChnlCd,
  341. 'Ittparty_Tms' => $this->getTimestamp(),
  342. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  343. 'Mkt_Id' => $this->mktId,
  344. 'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
  345. 'Rfnd_Trcno' => $Rfnd_Trcno,
  346. 'Vno' => '4',
  347. ];
  348. return $this->apiRequest($uri, $params);
  349. }
  350. /**
  351. * 5.3 确认收货接口
  352. * @param string $Prim_Ordr_No 银行主订单编号
  353. * @return array
  354. */
  355. public function mergeNoticeArrival(string $Prim_Ordr_No)
  356. {
  357. $uri = '/online/direct/enquireRefundOrder';
  358. $params = [
  359. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  360. 'Py_Chnl_Cd' => $this->pyChnlCd,
  361. 'Ittparty_Tms' => $this->getTimestamp(),
  362. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  363. 'Mkt_Id' => $this->mktId,
  364. 'Prim_Ordr_No' => $Prim_Ordr_No,
  365. 'Vno' => '4',
  366. ];
  367. return $this->apiRequest($uri, $params);
  368. }
  369. }