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.

628 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
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\Constants\v3\OrderState;
  7. use App\Exception\BusinessException;
  8. use App\Exception\ErrorCodeException;
  9. use App\Model\v3\CcbPayment;
  10. use App\Model\v3\OrderMain;
  11. use App\Model\v3\Store;
  12. use App\Model\v3\User;
  13. use Hyperf\Di\Annotation\Inject;
  14. use Hyperf\Guzzle\ClientFactory;
  15. use Hyperf\Logger\LoggerFactory;
  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. $result[] = ['', $value];
  166. }
  167. } else {
  168. $value = trim($item);
  169. if ($value !== '') {
  170. $result[] = [$key, $value];
  171. }
  172. }
  173. }
  174. $str = '';
  175. foreach ($result as [$key, $value]) {
  176. if ($value) {
  177. $str .= $key ? "$key=$value&" : "$value&";
  178. }
  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. return $this->apiRequest($uri, $params);
  323. }
  324. /**
  325. * 4.1 订单退款接口
  326. * @param string $Cust_Rfnd_Trcno 退款流水号,对于同一笔退款唯一
  327. * @param string $Py_Trn_No 银行支付流水号
  328. * @param string $Rfnd_Amt 退款金额,全额退款时可不传
  329. * @param array $Sub_Ordr_List 子订单列表,全额退款时不需要传该域
  330. * @return array
  331. */
  332. public function refundOrder(string $Cust_Rfnd_Trcno, string $Py_Trn_No, string $Rfnd_Amt = '', array $Sub_Ordr_List = [])
  333. {
  334. $uri = '/online/direct/refundOrder';
  335. $params = [
  336. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  337. 'Py_Chnl_Cd' => $this->pyChnlCd,
  338. 'Ittparty_Tms' => $this->getTimestamp(),
  339. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  340. 'Mkt_Id' => $this->mktId,
  341. 'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
  342. 'Py_Trn_No' => $Py_Trn_No,
  343. 'Rfnd_Amt' => $Rfnd_Amt,
  344. 'Sub_Ordr_List' => $Sub_Ordr_List,
  345. 'Vno' => '3',
  346. ];
  347. if ($Sub_Ordr_List) {
  348. $params['Sub_Ordr_List'] = $Sub_Ordr_List;
  349. }
  350. return $this->apiRequest($uri, $params);
  351. }
  352. /**
  353. * 退款子订单格式化
  354. * @param string $Sub_Ordr_Id 银行子订单编号
  355. * @param string $Rfnd_Amt 退款金额
  356. * @return string[]
  357. */
  358. public function refundSubOrderListItem(string $Sub_Ordr_Id, string $Rfnd_Amt)
  359. {
  360. return compact('Sub_Ordr_Id', 'Rfnd_Amt');
  361. }
  362. /**
  363. * 4.3 查询退款结果接口,同一笔退款不支持并发查询
  364. * @param string $Cust_Rfnd_Trcno 我方退款流水号,与银行退款流水号必输其一
  365. * @param string $Rfnd_Trcno 银行退款流水号
  366. * @return array
  367. */
  368. public function enquireRefundOrder(string $Cust_Rfnd_Trcno, string $Rfnd_Trcno = '')
  369. {
  370. $uri = '/online/direct/enquireRefundOrder';
  371. $params = [
  372. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  373. 'Py_Chnl_Cd' => $this->pyChnlCd,
  374. 'Ittparty_Tms' => $this->getTimestamp(),
  375. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  376. 'Mkt_Id' => $this->mktId,
  377. 'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
  378. 'Rfnd_Trcno' => $Rfnd_Trcno,
  379. 'Vno' => '4',
  380. ];
  381. return $this->apiRequest($uri, $params);
  382. }
  383. /**
  384. * 5.3 确认收货接口
  385. * @param string $Prim_Ordr_No 银行主订单编号
  386. * @return array
  387. */
  388. public function mergeNoticeArrival(string $Prim_Ordr_No)
  389. {
  390. $uri = '/online/direct/enquireRefundOrder';
  391. $params = [
  392. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  393. 'Py_Chnl_Cd' => $this->pyChnlCd,
  394. 'Ittparty_Tms' => $this->getTimestamp(),
  395. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  396. 'Mkt_Id' => $this->mktId,
  397. 'Prim_Ordr_No' => $Prim_Ordr_No,
  398. 'Vno' => '4',
  399. ];
  400. return $this->apiRequest($uri, $params);
  401. }
  402. /**
  403. * 10.1 订单信息查询
  404. * @param string $Main_Ordr_No 主订单编号
  405. * @param string $Py_Trn_No 支付流水号,主订单号与支付流水号必输其一
  406. * @return array
  407. */
  408. public function orderInfQuery(string $Main_Ordr_No, string $Py_Trn_No = '')
  409. {
  410. $uri = '/online/direct/OrderInfQuery';
  411. $params = [
  412. 'Ittparty_Stm_Id' => $this->ittpartyStmId,
  413. 'Py_Chnl_Cd' => $this->pyChnlCd,
  414. 'Ittparty_Tms' => $this->getTimestamp(),
  415. 'Ittparty_Jrnl_No' => $this->genSerialNumber(),
  416. 'Mkt_Id' => $this->mktId,
  417. 'Main_Ordr_No' => $Main_Ordr_No,
  418. 'Py_Trn_No' => $Py_Trn_No,
  419. 'Vno' => '4',
  420. ];
  421. return $this->apiRequest($uri, $params);
  422. }
  423. /**
  424. * 创建支付订单
  425. * @param $globalOrderId
  426. * @param $userId
  427. * @return array
  428. * @throws \GuzzleHttp\Exception\GuzzleException
  429. */
  430. public function createCcbPayment($globalOrderId, $userId)
  431. {
  432. try {
  433. // 待支付的,未超时(15min,900sec)的订单
  434. $orderMain = OrderMain::query()
  435. ->where(['state' => OrderState::UNPAID, 'global_order_id' => $globalOrderId, 'user_id' => $userId])
  436. ->where('created_at', '>=', (time() - 900))
  437. ->first();
  438. if (empty($orderMain)) {
  439. throw new BusinessException(ErrorCode::ORDER_NOT_AVAILABLE, '[订单号无效]'.$globalOrderId);
  440. }
  441. $user = User::select('openid')->find($userId);
  442. $model = new CcbPayment();
  443. $model->order_main_id = $orderMain->global_order_id;
  444. $model->main_ordr_no = $this->genMainOrderNo();
  445. $model->pymd_cd = $this->pymdCd;
  446. if ($this->pymdCd == '05') {
  447. $model->sub_appid = config('wechat.applet.app_id');
  448. $model->sub_openid = $user->openid;
  449. }
  450. $model->py_ordr_tpcd = '04';
  451. $model->ordr_tamt = $orderMain->money;
  452. $model->txn_tamt = $orderMain->money;
  453. $subOrderList = [];
  454. // 运费判断
  455. if (bccomp($orderMain->delivery_money, '0', 2) == 1) {
  456. $subOrderList[] = $this->subOrderListItem(
  457. $this->merchantId,
  458. $model->main_ordr_no.'D',
  459. $orderMain->delivery_money,
  460. $orderMain->delivery_money,
  461. '配送费'
  462. );
  463. }
  464. // 我方分润比例%
  465. $selfProfitRatio = config('ccb.self_profit_ratio');
  466. if (bccomp($selfProfitRatio, '0', 3) == 0) {
  467. throw new BusinessException(500, '[未配置分润比例]');
  468. }
  469. foreach ($orderMain->orders as $order) {
  470. if (empty($order->store->ccb_merchant_id)) {
  471. throw new BusinessException(500, '[店铺未配置商家编号]'.$order->store_id);
  472. }
  473. foreach ($order->orderGoods as $orderGoods) {
  474. $goodsMoney = bcmul($orderGoods->price, (string)$orderGoods->number, 2);
  475. $selfProfitMoney = bcmul($goodsMoney, $selfProfitRatio, 2);
  476. $merchantProfitMoney = bcsub($goodsMoney, $selfProfitMoney, 2);
  477. // 平台抽佣后最小金额不能为0
  478. if (bccomp($merchantProfitMoney, '0', 2) == 0) {
  479. $selfProfitMoney = '0.00';
  480. $merchantProfitMoney = $goodsMoney;
  481. }
  482. $subOrderList[] = $this->subOrderListItem(
  483. $order->store->ccb_merchant_id,
  484. $model->main_ordr_no.'G'.$orderGoods->id.'N01',
  485. $merchantProfitMoney,
  486. $merchantProfitMoney,
  487. $orderGoods->name.'(分佣到账)'
  488. );
  489. if (bccomp($selfProfitMoney, '0', 2) == 1) {
  490. $subOrderList[] = $this->subOrderListItem(
  491. $this->merchantId,
  492. $model->main_ordr_no.'G'.$orderGoods->id.'N02',
  493. $selfProfitMoney,
  494. $selfProfitMoney,
  495. $orderGoods->name.'(平台抽佣)'
  496. );
  497. }
  498. }
  499. }
  500. $model->orderlist = json_encode($subOrderList);
  501. $model->save();
  502. $result = $this->gatherPlaceorder(
  503. $model->main_ordr_no,
  504. $model->ordr_tamt,
  505. $model->txn_tamt,
  506. $model->sub_appid,
  507. $model->sub_openid,
  508. $subOrderList
  509. );
  510. $model->py_trn_no = $result['Py_Trn_No'];
  511. $model->prim_ordr_no = $result['Prim_Ordr_No'];
  512. $model->ordr_gen_tm = $result['Ordr_Gen_Tm'];
  513. if (isset($result['Cshdk_Url'])) {
  514. $model->cshdk_url = $result['Cshdk_Url'];
  515. }
  516. if ($this->pymdCd == '05') {
  517. $model->rtn_par_data = json_encode($result['Rtn_Par_Data']);
  518. }
  519. $model->ordr_stcd = $result['Ordr_Stcd'];
  520. $model->rtn_orderlist = json_encode($result['Orderlist']);
  521. $model->save();
  522. if ($this->pymdCd == '05') {
  523. // 返回支付参数给前端
  524. $parameters = [
  525. 'appId' => $result['Rtn_Par_Data']['appId'],
  526. 'timeStamp' => $result['Rtn_Par_Data']['timeStamp'],
  527. 'nonceStr' => $result['Rtn_Par_Data']['nonceStr'],
  528. 'package' => $result['Rtn_Par_Data']['package'],
  529. 'signType' => $result['Rtn_Par_Data']['signType'],
  530. 'paySign' => $result['Rtn_Par_Data']['paySign'],
  531. ];
  532. } else {
  533. $parameters = [
  534. 'Cshdk_Url' => $model->cshdk_url,
  535. ];
  536. }
  537. $parameters['order_main_id'] = $orderMain->global_order_id;
  538. return $parameters;
  539. } catch (\Exception $e) {
  540. $this->log->event(LogLabel::ORDER_PAYMENT_LOG, ['payment_do_exception_msg' => $e->getMessage()]);
  541. $message = $e instanceof BusinessException ? $e->getMessage() : '[稍后重试]';
  542. throw new ErrorCodeException(ErrorCode::PAYMENT_FAIL, $message);
  543. }
  544. }
  545. }