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.

641 lines
20 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. <?php
  2. namespace App\Service\v3;
  3. use App\Commons\Log;
  4. use App\Constants\v3\ErrorCode;
  5. use App\Constants\v3\LogLabel;
  6. use App\Exception\BusinessException;
  7. use App\Exception\ErrorCodeException;
  8. use App\Model\v3\CcbPayment;
  9. use App\Model\v3\OrderMain;
  10. use App\Model\v3\User;
  11. use App\TaskWorker\SSDBTask;
  12. use Hyperf\Di\Annotation\Inject;
  13. use Hyperf\Guzzle\ClientFactory;
  14. use Hyperf\Logger\LoggerFactory;
  15. use Hyperf\Utils\ApplicationContext;
  16. class CcbPaymentService
  17. {
  18. /**
  19. * 是否测试环境
  20. * @var bool
  21. */
  22. private $isDebug;
  23. /**
  24. * 测试环境请求的域名
  25. * @var string
  26. */
  27. private $devBaseUri = 'http://marketpayktwo.dev.jh:8028';
  28. /**
  29. * 生产环境请求的域名
  30. * @var string
  31. */
  32. private $prodBaseUri = 'https://marketpay.ccb.com';
  33. /**
  34. * 市场编号
  35. * @var string
  36. */
  37. private $mktId;
  38. /**
  39. * 我方商家编号
  40. * @var string
  41. */
  42. private $merchantId;
  43. /**
  44. * 支付方式
  45. * @var string
  46. */
  47. private $pymdCd;
  48. /**
  49. * 我方私钥
  50. * @var resource
  51. */
  52. private $selfPrivateKey;
  53. /**
  54. * 银行公钥
  55. * @var resource
  56. */
  57. private $bankPublicKey;
  58. /**
  59. * 发起渠道编号
  60. * @var string
  61. */
  62. private $ittpartyStmId = '00000';
  63. /**
  64. * 支付渠道代码
  65. * @var string
  66. */
  67. private $pyChnlCd = '0000000000000000000000000';
  68. /**
  69. * @var \Hyperf\Guzzle\ClientFactory
  70. */
  71. private $clientFactory;
  72. /**
  73. * @var \Psr\Log\LoggerInterface
  74. */
  75. private $logger;
  76. /**
  77. * @Inject
  78. * @var Log
  79. */
  80. protected $log;
  81. public function __construct(ClientFactory $clientFactory, LoggerFactory $loggerFactory)
  82. {
  83. $this->clientFactory = $clientFactory;
  84. $this->logger = $loggerFactory->get('ccb');
  85. $this->isDebug = config('ccb.debug');
  86. $this->mktId = config('ccb.mkt_id');
  87. $this->merchantId = config('ccb.merchant_id');
  88. $this->pymdCd = config('ccb.pymd_cd');
  89. $selfPrivateKey = config('ccb.self_private_key');
  90. $selfPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split($selfPrivateKey, 64, "\n")."-----END RSA PRIVATE KEY-----\n";
  91. $this->selfPrivateKey = openssl_get_privatekey($selfPrivateKey);
  92. $bankPublicKey = config('ccb.bank_public_key');
  93. $bankPublicKey = "-----BEGIN PUBLIC KEY-----\n".chunk_split($bankPublicKey, 64, "\n")."-----END PUBLIC KEY-----\n";
  94. $this->bankPublicKey = openssl_get_publickey($bankPublicKey);
  95. }
  96. /**
  97. * 使用我方私钥加密
  98. * @param string $data
  99. * @return string
  100. */
  101. public function encrypt(string $data)
  102. {
  103. $str = '';
  104. foreach (str_split($data, 117) as $chunk) {
  105. openssl_private_encrypt($chunk, $crypted, $this->selfPrivateKey);
  106. $str .= $crypted;
  107. }
  108. return base64_encode($str);
  109. }
  110. /**
  111. * 使用银行公钥解密
  112. * @param string $data
  113. * @return string
  114. */
  115. public function decrypt(string $data)
  116. {
  117. $raw = base64_decode($data);
  118. $str = '';
  119. foreach (str_split($raw, 256) as $chunk) {
  120. openssl_public_decrypt($chunk, $decrypted, $this->bankPublicKey);
  121. $str .= $decrypted;
  122. }
  123. return $str;
  124. }
  125. /**
  126. * 使用我方私钥生成签名
  127. * @param string $data
  128. * @return string
  129. */
  130. public function sign(string $data)
  131. {
  132. openssl_sign($data, $signature, $this->selfPrivateKey, OPENSSL_ALGO_SHA256);
  133. return base64_encode($signature);
  134. }
  135. /**
  136. * 使用银行公钥验证签名
  137. * @param string $data
  138. * @param string $signature
  139. * @return bool
  140. */
  141. public function verifySign(string $data, string $signature)
  142. {
  143. $signature = base64_decode($signature);
  144. $result = openssl_verify($data, $signature, $this->bankPublicKey, OPENSSL_ALGO_SHA256);
  145. return $result == 1;
  146. }
  147. /**
  148. * 计算待签名字符串
  149. * @param array $params
  150. * @return string
  151. */
  152. public function createSign(array $params)
  153. {
  154. // 不参与签名的字符串
  155. $unsignKeys = ['Sign_Inf', 'Svc_Rsp_St', 'Svc_Rsp_Cd', 'Rsp_Inf'];
  156. $result = [];
  157. ksort($params);
  158. foreach ($params as $key => $item) {
  159. if (in_array($key, $unsignKeys)) {
  160. continue;
  161. }
  162. if (is_array($item)) {
  163. foreach ($item as $child) {
  164. $value = $this->createSign($child);
  165. if ($value !== '') {
  166. $result[] = ['', $value];
  167. }
  168. }
  169. } else {
  170. $value = trim($item);
  171. if ($value !== '') {
  172. $result[] = [$key, $value];
  173. }
  174. }
  175. }
  176. $str = '';
  177. foreach ($result as [$key, $value]) {
  178. $str .= $key ? "$key=$value&" : "$value&";
  179. }
  180. return rtrim($str, '&');
  181. }
  182. /**
  183. * 发送API请求
  184. * @param string $uri
  185. * @param array $params
  186. * @return array
  187. */
  188. private function apiRequest(string $uri, array $params = []): array
  189. {
  190. $signData = $this->createSign($params);
  191. $params['Sign_Inf'] = $this->sign($signData);
  192. if ($this->isDebug) {
  193. $uri = $this->devBaseUri.$uri;
  194. } else {
  195. $uri = $this->prodBaseUri.$uri;
  196. }
  197. $options = ['json' => $params];
  198. $startTime = microtime(true);
  199. try {
  200. $response = $this->clientFactory->create(['timeout' => 60])->post($uri, $options);
  201. } catch (\Exception $e) {
  202. $this->saveApiLog(0, $uri, $params, $e->getMessage());
  203. throw new BusinessException(500, '请求异常');
  204. }
  205. $useTime = round((microtime(true) - $startTime) * 1000, 2);
  206. $content = $response->getBody()->getContents();
  207. $result = json_decode($content, true);
  208. if (!isset($result['Svc_Rsp_St']) || $result['Svc_Rsp_St'] != '00') {
  209. $this->saveApiLog($useTime, $uri, $params, $content);
  210. throw new BusinessException(500, (($result['Rsp_Inf'] ?? '') ?: 'CCB请求失败'));
  211. }
  212. if (!isset($result['Sign_Inf']) || !$this->verifySign($this->createSign($result), $result['Sign_Inf'])) {
  213. $this->saveApiLog($useTime, $uri, $params, $content);
  214. throw new BusinessException(500, (($result['Rsp_Inf'] ?? '') ?: 'CCB验签失败'));
  215. }
  216. if ($this->isDebug) {
  217. $this->saveApiLog($useTime, $uri, $params, $content);
  218. }
  219. return $result;
  220. }
  221. /**
  222. * 保存API请求日志
  223. * @param $useTime
  224. * @param $uri
  225. * @param $params
  226. * @param $content
  227. * @return void
  228. */
  229. private function saveApiLog($useTime, $uri, $params, $content)
  230. {
  231. $this->logger->info(
  232. sprintf(
  233. "%s\nTime: %.2f ms\nUrl: %s\nParams:\n%s\nContent:\n%s\n",
  234. 'CCB API Log',
  235. $useTime,
  236. $uri,
  237. json_encode($params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
  238. $content
  239. )
  240. );
  241. }
  242. private function getTimestamp()
  243. {
  244. return (new \DateTime())->format('YmdHisv');
  245. }
  246. private function genSerialNumber()
  247. {
  248. return date('YmdHis').mt_rand(10000, 99999).mt_rand(10000, 99999);
  249. }
  250. private function genMainOrderNo()
  251. {
  252. return time().mt_rand(10000, 99999).mt_rand(10000, 99999);
  253. }
  254. /**
  255. * 3.1 生成支付订单接口
  256. * @param string $Main_Ordr_No 主订单编号
  257. * @param string $Ordr_Tamt 订单总金额
  258. * @param string $Txn_Tamt 交易总金额(实付)
  259. * @param string|null $Sub_Appid 当前调起支付的小程序appid
  260. * @param string|null $Sub_Openid 用户在小程序下的openid
  261. * @param array $Orderlist 子订单列表
  262. * @return array
  263. */
  264. public function gatherPlaceorder(string $Main_Ordr_No, string $Ordr_Tamt, string $Txn_Tamt, ?string $Sub_Appid, ?string $Sub_Openid, array $Orderlist)
  265. {
  266. $uri = '/online/direct/gatherPlaceorder';
  267. $params = [
  268. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  269. 'Py_Chnl_Cd' => $this->pyChnlCd,
  270. 'Ittparty_Tms' => $this->getTimestamp(),
  271. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  272. 'Mkt_Id' => $this->mktId,
  273. 'Main_Ordr_No' => $Main_Ordr_No,
  274. 'Pymd_Cd' => $this->pymdCd,
  275. 'Py_Ordr_Tpcd' => '04',
  276. 'Py_Rslt_Ntc_Sn' => '1',
  277. 'Ccy' => '156',
  278. 'Ordr_Tamt' => $Ordr_Tamt,
  279. 'Txn_Tamt' => $Txn_Tamt,
  280. 'Order_Time_Out' => '1800',
  281. 'Orderlist' => $Orderlist,
  282. 'Vno' => '4',
  283. ];
  284. if ($this->pymdCd == '05') {
  285. $params['Sub_Appid'] = $Sub_Appid;
  286. $params['Sub_Openid'] = $Sub_Openid;
  287. }
  288. return $this->apiRequest($uri, $params);
  289. }
  290. /**
  291. * 下单子订单格式化
  292. * @param string $Mkt_Mrch_Id 商家编号
  293. * @param string $Cmdty_Ordr_No 子订单编号
  294. * @param string $Ordr_Amt 订单金额
  295. * @param string $Txnamt 实付金额
  296. * @param string $Cmdty_Dsc 商品描述
  297. * @return string[]
  298. */
  299. public function subOrderListItem(string $Mkt_Mrch_Id, string $Cmdty_Ordr_No, string $Ordr_Amt, string $Txnamt, string $Cmdty_Dsc)
  300. {
  301. return compact('Mkt_Mrch_Id', 'Cmdty_Ordr_No', 'Ordr_Amt', 'Txnamt', 'Cmdty_Dsc');
  302. }
  303. /**
  304. * 3.4 查询支付结果接口,同一笔订单不支持并发查询
  305. * @param string $Main_Ordr_No 主订单号,主订单号与支付流水号必输其一
  306. * @param string $Py_Trn_No 银行支付流水号
  307. * @return array
  308. */
  309. public function gatherEnquireOrder(string $Main_Ordr_No, string $Py_Trn_No = '')
  310. {
  311. $uri = '/online/direct/gatherEnquireOrder';
  312. $params = [
  313. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  314. 'Py_Chnl_Cd' => $this->pyChnlCd,
  315. 'Ittparty_Tms' => $this->getTimestamp(),
  316. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  317. 'Mkt_Id' => $this->mktId,
  318. 'Main_Ordr_No' => $Main_Ordr_No,
  319. 'Py_Trn_No' => $Py_Trn_No,
  320. 'Vno' => '4',
  321. ];
  322. $ssdb = ApplicationContext::getContainer()->get(SSDBTask::class);
  323. $key = 'ccb_'.__FUNCTION__.'_'.$Main_Ordr_No.$Py_Trn_No;
  324. if ($ssdb->exec('setnx',$key, 1)) {
  325. try {
  326. return $this->apiRequest($uri, $params);
  327. } finally {
  328. $ssdb->exec('expire', $key, 2);
  329. }
  330. } else {
  331. sleep(1);
  332. return $this->gatherEnquireOrder($Main_Ordr_No, $Py_Trn_No);
  333. }
  334. }
  335. /**
  336. * 4.1 订单退款接口
  337. * @param string $Cust_Rfnd_Trcno 退款流水号,对于同一笔退款唯一
  338. * @param string $Py_Trn_No 银行支付流水号
  339. * @param string $Rfnd_Amt 退款金额,全额退款时可不传
  340. * @param array $Sub_Ordr_List 子订单列表,全额退款时不需要传该域
  341. * @return array
  342. */
  343. public function refundOrder(string $Cust_Rfnd_Trcno, string $Py_Trn_No, string $Rfnd_Amt = '', array $Sub_Ordr_List = [])
  344. {
  345. $uri = '/online/direct/refundOrder';
  346. $params = [
  347. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  348. 'Py_Chnl_Cd' => $this->pyChnlCd,
  349. 'Ittparty_Tms' => $this->getTimestamp(),
  350. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  351. 'Mkt_Id' => $this->mktId,
  352. 'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
  353. 'Py_Trn_No' => $Py_Trn_No,
  354. 'Rfnd_Amt' => $Rfnd_Amt,
  355. 'Sub_Ordr_List' => $Sub_Ordr_List,
  356. 'Vno' => '3',
  357. ];
  358. if ($Sub_Ordr_List) {
  359. $params['Sub_Ordr_List'] = $Sub_Ordr_List;
  360. }
  361. return $this->apiRequest($uri, $params);
  362. }
  363. /**
  364. * 退款子订单格式化
  365. * @param string $Sub_Ordr_Id 银行子订单编号
  366. * @param string $Rfnd_Amt 退款金额
  367. * @return string[]
  368. */
  369. public function refundSubOrderListItem(string $Sub_Ordr_Id, string $Rfnd_Amt)
  370. {
  371. return compact('Sub_Ordr_Id', 'Rfnd_Amt');
  372. }
  373. /**
  374. * 4.3 查询退款结果接口,同一笔退款不支持并发查询
  375. * @param string $Cust_Rfnd_Trcno 我方退款流水号,与银行退款流水号必输其一
  376. * @param string $Rfnd_Trcno 银行退款流水号
  377. * @return array
  378. */
  379. public function enquireRefundOrder(string $Cust_Rfnd_Trcno, string $Rfnd_Trcno = '')
  380. {
  381. $uri = '/online/direct/enquireRefundOrder';
  382. $params = [
  383. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  384. 'Py_Chnl_Cd' => $this->pyChnlCd,
  385. 'Ittparty_Tms' => $this->getTimestamp(),
  386. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  387. 'Mkt_Id' => $this->mktId,
  388. 'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
  389. 'Rfnd_Trcno' => $Rfnd_Trcno,
  390. 'Vno' => '4',
  391. ];
  392. return $this->apiRequest($uri, $params);
  393. }
  394. /**
  395. * 5.3 确认收货接口
  396. * @param string $Prim_Ordr_No 银行主订单编号
  397. * @return array
  398. */
  399. public function mergeNoticeArrival(string $Prim_Ordr_No)
  400. {
  401. $uri = '/online/direct/enquireRefundOrder';
  402. $params = [
  403. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  404. 'Py_Chnl_Cd' => $this->pyChnlCd,
  405. 'Ittparty_Tms' => $this->getTimestamp(),
  406. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  407. 'Mkt_Id' => $this->mktId,
  408. 'Prim_Ordr_No' => $Prim_Ordr_No,
  409. 'Vno' => '4',
  410. ];
  411. return $this->apiRequest($uri, $params);
  412. }
  413. /**
  414. * 10.1 订单信息查询
  415. * @param string $Main_Ordr_No 主订单编号
  416. * @param string $Py_Trn_No 支付流水号,主订单号与支付流水号必输其一
  417. * @return array
  418. */
  419. public function orderInfQuery(string $Main_Ordr_No, string $Py_Trn_No = '')
  420. {
  421. $uri = '/online/direct/OrderInfQuery';
  422. $params = [
  423. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  424. 'Py_Chnl_Cd' => $this->pyChnlCd,
  425. 'Ittparty_Tms' => $this->getTimestamp(),
  426. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  427. 'Mkt_Id' => $this->mktId,
  428. 'Main_Ordr_No' => $Main_Ordr_No,
  429. 'Py_Trn_No' => $Py_Trn_No,
  430. 'Vno' => '4',
  431. ];
  432. return $this->apiRequest($uri, $params);
  433. }
  434. /**
  435. * 创建支付订单
  436. * @param $globalOrderId
  437. * @param $userId
  438. * @return array
  439. * @throws \GuzzleHttp\Exception\GuzzleException
  440. */
  441. public function createCcbPayment($globalOrderId, $userId)
  442. {
  443. try {
  444. // 待支付的,未超时(15min,900sec)的订单
  445. $orderMain = OrderMain::query()
  446. ->where(['state' => OrderState::UNPAID, 'global_order_id' => $globalOrderId, 'user_id' => $userId])
  447. ->where('created_at', '>=', (time() - 900))
  448. ->first();
  449. if (empty($orderMain)) {
  450. throw new BusinessException(ErrorCode::ORDER_NOT_AVAILABLE, '[订单号无效]'.$globalOrderId);
  451. }
  452. $user = User::select('openid')->find($userId);
  453. $model = new CcbPayment();
  454. $model->order_main_id = $orderMain->global_order_id;
  455. $model->main_ordr_no = $this->genMainOrderNo();
  456. $model->pymd_cd = $this->pymdCd;
  457. if ($this->pymdCd == '05') {
  458. $model->sub_appid = config('wechat.applet.app_id');
  459. $model->sub_openid = $user->openid;
  460. }
  461. $model->py_ordr_tpcd = '04';
  462. $model->ordr_tamt = $orderMain->money;
  463. $model->txn_tamt = $orderMain->money;
  464. $subOrderList = [];
  465. // 运费判断
  466. if (bccomp($orderMain->delivery_money, '0', 2) == 1) {
  467. $subOrderList[] = $this->subOrderListItem(
  468. $this->merchantId,
  469. $model->main_ordr_no.'D',
  470. $orderMain->delivery_money,
  471. $orderMain->delivery_money,
  472. '配送费'
  473. );
  474. }
  475. // 我方分润比例%
  476. $selfProfitRatio = config('ccb.self_profit_ratio');
  477. if (bccomp($selfProfitRatio, '0', 3) == 0) {
  478. throw new BusinessException(500, '[未配置分润比例]');
  479. }
  480. foreach ($orderMain->orders as $order) {
  481. if (empty($order->store->ccb_merchant_id)) {
  482. throw new BusinessException(500, '[店铺未配置商家编号]'.$order->store_id);
  483. }
  484. foreach ($order->orderGoods as $orderGoods) {
  485. $goodsMoney = bcmul($orderGoods->price, (string)$orderGoods->number, 2);
  486. $selfProfitMoney = bcmul($goodsMoney, $selfProfitRatio, 2);
  487. $merchantProfitMoney = bcsub($goodsMoney, $selfProfitMoney, 2);
  488. // 平台抽佣后最小金额不能为0
  489. if (bccomp($merchantProfitMoney, '0', 2) == 0) {
  490. $selfProfitMoney = '0.00';
  491. $merchantProfitMoney = $goodsMoney;
  492. }
  493. $subOrderList[] = $this->subOrderListItem(
  494. $order->store->ccb_merchant_id,
  495. $model->main_ordr_no.'G'.$orderGoods->id.'N01',
  496. $merchantProfitMoney,
  497. $merchantProfitMoney,
  498. $orderGoods->name.'(分佣到账)'
  499. );
  500. if (bccomp($selfProfitMoney, '0', 2) == 1) {
  501. $subOrderList[] = $this->subOrderListItem(
  502. $this->merchantId,
  503. $model->main_ordr_no.'G'.$orderGoods->id.'N02',
  504. $selfProfitMoney,
  505. $selfProfitMoney,
  506. $orderGoods->name.'(平台抽佣)'
  507. );
  508. }
  509. }
  510. }
  511. $model->orderlist = json_encode($subOrderList);
  512. $model->save();
  513. $result = $this->gatherPlaceorder(
  514. $model->main_ordr_no,
  515. $model->ordr_tamt,
  516. $model->txn_tamt,
  517. $model->sub_appid,
  518. $model->sub_openid,
  519. $subOrderList
  520. );
  521. $model->py_trn_no = $result['Py_Trn_No'];
  522. $model->prim_ordr_no = $result['Prim_Ordr_No'];
  523. $model->ordr_gen_tm = $result['Ordr_Gen_Tm'];
  524. if (isset($result['Cshdk_Url'])) {
  525. $model->cshdk_url = $result['Cshdk_Url'];
  526. }
  527. if ($this->pymdCd == '05') {
  528. $model->rtn_par_data = json_encode($result['Rtn_Par_Data']);
  529. }
  530. $model->ordr_stcd = $result['Ordr_Stcd'];
  531. $model->rtn_orderlist = json_encode($result['Orderlist']);
  532. $model->save();
  533. if ($this->pymdCd == '05') {
  534. // 返回支付参数给前端
  535. $parameters = [
  536. 'appId' => $result['Rtn_Par_Data']['appId'],
  537. 'timeStamp' => $result['Rtn_Par_Data']['timeStamp'],
  538. 'nonceStr' => $result['Rtn_Par_Data']['nonceStr'],
  539. 'package' => $result['Rtn_Par_Data']['package'],
  540. 'signType' => $result['Rtn_Par_Data']['signType'],
  541. 'paySign' => $result['Rtn_Par_Data']['paySign'],
  542. ];
  543. } else {
  544. $parameters = [
  545. 'Cshdk_Url' => $model->cshdk_url,
  546. ];
  547. }
  548. $parameters['order_main_id'] = $orderMain->global_order_id;
  549. return $parameters;
  550. } catch (\Exception $e) {
  551. $this->log->event(LogLabel::ORDER_PAYMENT_LOG, ['payment_do_exception_msg' => $e->getMessage()]);
  552. $message = $e instanceof BusinessException ? $e->getMessage() : '[稍后重试]';
  553. throw new ErrorCodeException(ErrorCode::PAYMENT_FAIL, $message);
  554. }
  555. }
  556. }