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.

459 lines
14 KiB

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