SandPay.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. <?php
  2. namespace App\Libs\Pay;
  3. use Exception;
  4. use GuzzleHttp\Client;
  5. class SandPay
  6. {
  7. const API_HOST = 'https://caspay.sandpay.com.cn/agent-main/openapi/';
  8. const API_TEST_HOST = 'http://61.129.71.103:7970/agent-main/openapi/';
  9. const NOTIFY_HOST = 'http://116.62.175.234:8094/';
  10. const NOTIFY_TEST_HOST = 'http://managepre.aizhuishu.com/';
  11. const PUB_PRODUCT_ID = '00000003'; //代付对公:00000003
  12. const PRI_PRODUCT_ID = '00000004'; //代付对私:00000004
  13. const PUB_KEY_PATH = 'cert/sandpay-finance/sand.cer'; //公钥文件
  14. const PRI_KEY_PATH = 'cert/sandpay-finance/vPvWnHjBeQhHLebn.pfx'; //私钥文件
  15. const CERT_PWD = 'vPvWnHjBeQhHLebn'; //私钥证书密码
  16. const MER_ID = '6888804025954';
  17. private $notify_url;
  18. private $agentpay_url;
  19. private $query_balance_url;
  20. private $query_order_url;
  21. private $private_key;
  22. private $public_key;
  23. private $client;
  24. private $product_id;
  25. public function __construct(bool $is_private = true)
  26. {
  27. $this->agentpay_url = (self::API_HOST) . 'agentpay';
  28. $this->query_balance_url = (self::API_HOST) . 'queryBalance';
  29. $this->query_order_url = (self::API_HOST) . 'queryOrder';
  30. $this->private_key = $this->loadPk12Cert(storage_path(self::PRI_KEY_PATH), self::CERT_PWD);
  31. $this->public_key = $this->loadX509Cert(storage_path(self::PUB_KEY_PATH));
  32. $this->notify_url = (env('APP_ENV') == 'online' ? self::NOTIFY_HOST : self::NOTIFY_TEST_HOST) . 'api/tradepayapi/sandPayNotify';
  33. $this->product_id = $is_private ? self::PRI_PRODUCT_ID : self::PUB_PRODUCT_ID;
  34. $this->client = new Client();
  35. }
  36. /**
  37. * 格式金额
  38. */
  39. private function amountFormat(float $amount)
  40. {
  41. $amount = strval($amount * 100);
  42. $length = strlen($amount);
  43. for ($i = 0; $i < 12 - $length; $i++) {
  44. $amount = '0' . $amount;
  45. }
  46. return $amount;
  47. }
  48. /**
  49. * 反格式化金额
  50. */
  51. private function formatAmount(string $amount)
  52. {
  53. return (float) (((int) ltrim($amount, '\0')) / 100);
  54. }
  55. /**
  56. * 实时代付
  57. * @param array $data
  58. * @return array
  59. */
  60. public function agentPay(array $data)
  61. {
  62. $params = [
  63. 'transCode' => 'RTPM', // 实时代付
  64. 'url' => $this->agentpay_url,
  65. 'params' => [
  66. 'version' => '01',
  67. 'productId' => $this->product_id,
  68. 'tranTime' => date('YmdHis', strtotime($data['pay_time'])),
  69. 'orderCode' => $data['order_no'],
  70. 'tranAmt' => $this->amountFormat($data['amount']),
  71. 'currencyCode' => '156',
  72. 'accAttr' => $this->product_id == self::PRI_PRODUCT_ID ? '0' : '1',
  73. 'accType' => $this->product_id == self::PRI_PRODUCT_ID ? '4' : '3',
  74. 'accNo' => $data['bank_account'],
  75. 'accName' => $data['account_name'],
  76. 'bankName' => $data['account_bank'],
  77. 'remark' => $data['remark'],
  78. 'payMode' => '1',
  79. 'channelType' => '07',
  80. 'noticeUrl' => $this->notify_url,
  81. ],
  82. ];
  83. if ($this->product_id == self::PUB_PRODUCT_ID) {
  84. $params['params']['bankType'] = $data['bank_number'];
  85. }
  86. $result = $this->send($params);
  87. if ($result->resultFlag == 0) {
  88. return [
  89. 'result' => true,
  90. 'content' => $result,
  91. ];
  92. } else {
  93. return [
  94. 'result' => false,
  95. 'content' => $result,
  96. ];
  97. }
  98. }
  99. /**
  100. * 查询订单
  101. * @param array $data
  102. * @return array
  103. */
  104. public function queryOrder(array $data, bool $is_private)
  105. {
  106. $this->product_id = $is_private ? self::PRI_PRODUCT_ID : self::PUB_PRODUCT_ID;
  107. $params = [
  108. 'transCode' => 'ODQU', // 订单查询
  109. 'url' => $this->query_order_url,
  110. 'params' => [
  111. 'orderCode' => $data['order_no'],
  112. 'version' => '01',
  113. 'productId' => $this->product_id,
  114. 'tranTime' => date('YmdHis', strtotime($data['pay_time'])),
  115. ],
  116. ];
  117. $result = $this->send($params);
  118. if (isset($result->resultFlag) && $result->resultFlag == 0) {
  119. return [
  120. 'result' => 'success',
  121. 'content' => $result,
  122. ];
  123. } else if (isset($result->resultFlag) && $result->resultFlag == 2) {
  124. return [
  125. 'result' => 'waitting',
  126. 'content' => $result,
  127. ];
  128. } else {
  129. return [
  130. 'result' => 'failure',
  131. 'content' => $result,
  132. ];
  133. }
  134. }
  135. /**
  136. * 查询余额
  137. * @param array $data
  138. * @return float
  139. */
  140. public function queryBalance(array $data)
  141. {
  142. $params = [
  143. 'transCode' => 'MBQU', // 商户余额查询
  144. 'url' => $this->query_balance_url,
  145. 'params' => array(
  146. 'orderCode' => $data['order_no'],
  147. 'version' => '01',
  148. 'productId' => $this->product_id,
  149. 'tranTime' => date('YmdHis', strtotime($data['pay_time'])),
  150. )
  151. ];
  152. $result = $this->send($params);
  153. if (isset($result->balance)) {
  154. return ((int) ltrim($result->balance, '+')) / 100;
  155. } else {
  156. return 0;
  157. }
  158. }
  159. /**
  160. * 发送请求
  161. * @param $url
  162. * @param $param
  163. * @return array|null
  164. * @throws Exception
  165. */
  166. private function http_post_json(string $url, array $param)
  167. {
  168. try {
  169. $response = $this->client->post($url, ['form_params' => $param, 'Content-Type' => 'application/x-www-form-urlencoded']);
  170. $content = $response->getBody()->getContents();
  171. parse_str($content, $result);
  172. return $result;
  173. } catch (Exception $e) {
  174. throw $e;
  175. }
  176. }
  177. /**
  178. * 请求接口
  179. * @param array $data
  180. * @return object
  181. */
  182. private function send(array $data)
  183. {
  184. // step1: 生成AESKey并使用公钥加密
  185. $AESKey = $this->aes_generate(16);
  186. $encryptKey = $this->RSAEncryptByPub($AESKey, $this->public_key);
  187. $params = $data['params'];
  188. // step2: 使用AESKey加密报文
  189. $encryptData = $this->AESEncrypt($params, $AESKey);
  190. // step3: 使用私钥签名报文
  191. $sign = $this->sign($params, $this->private_key);
  192. // step4: 拼接post数据
  193. $post = [
  194. 'transCode' => $data['transCode'],
  195. 'accessType' => '0',
  196. 'merId' => self::MER_ID,
  197. 'encryptKey' => $encryptKey,
  198. 'encryptData' => $encryptData,
  199. 'sign' => $sign
  200. ];
  201. try {
  202. // step5: post请求
  203. $encrypt_data = $this->http_post_json($data['url'], $post);
  204. // step6: 使用私钥解密AESKey
  205. $decryptAESKey = $this->RSADecryptByPri($encrypt_data['encryptKey'], $this->private_key);
  206. // step7: 使用解密后的AESKey解密报文
  207. $decryptPlainText = $this->AESDecrypt($encrypt_data['encryptData'], $decryptAESKey);
  208. // step8: 使用公钥验签报文
  209. $this->verify($decryptPlainText, $encrypt_data['sign'], $this->public_key);
  210. $result = json_decode($decryptPlainText);
  211. if ($result->respCode != '0000') {
  212. myLog('sand_pay')->error($params['orderCode'] . ': ' . $decryptPlainText);
  213. } else {
  214. myLog('sand_pay')->info($decryptPlainText);
  215. }
  216. return $result;
  217. } catch (Exception $e) {
  218. myLog('sand_pay')->error($params['orderCode'] . ': ' . $e->getMessage());
  219. }
  220. return [];
  221. }
  222. /**
  223. * 获取公钥
  224. * @param $path
  225. * @return mixed
  226. * @throws Exception
  227. */
  228. private function loadX509Cert($path)
  229. {
  230. try {
  231. $file = file_get_contents($path);
  232. if (!$file) {
  233. throw new Exception('loadx509Cert::file_get_contents ERROR');
  234. }
  235. $cert = chunk_split(base64_encode($file), 64, "\n");
  236. $cert = "-----BEGIN CERTIFICATE-----\n" . $cert . "-----END CERTIFICATE-----\n";
  237. $res = openssl_pkey_get_public($cert);
  238. $detail = openssl_pkey_get_details($res);
  239. openssl_free_key($res);
  240. if (!$detail) {
  241. throw new Exception('loadX509Cert::openssl_pkey_get_details ERROR');
  242. }
  243. return $detail['key'];
  244. } catch (Exception $e) {
  245. throw $e;
  246. }
  247. }
  248. /**
  249. * 获取私钥
  250. * @param $path
  251. * @param $pwd
  252. * @return mixed
  253. * @throws Exception
  254. */
  255. private function loadPk12Cert($path, $pwd)
  256. {
  257. try {
  258. $file = file_get_contents($path);
  259. if (!$file) {
  260. throw new Exception('loadPk12Cert::file
  261. _get_contents');
  262. }
  263. if (!openssl_pkcs12_read($file, $cert, $pwd)) {
  264. throw new Exception('loadPk12Cert::openssl_pkcs12_read ERROR');
  265. }
  266. return $cert['pkey'];
  267. } catch (Exception $e) {
  268. throw $e;
  269. }
  270. }
  271. /**
  272. * 私钥签名
  273. * @param $plainText
  274. * @param $path
  275. * @return string
  276. * @throws Exception
  277. */
  278. function sign($plainText, $path)
  279. {
  280. $plainText = json_encode($plainText);
  281. try {
  282. $resource = openssl_pkey_get_private($path);
  283. $result = openssl_sign($plainText, $sign, $resource);
  284. openssl_free_key($resource);
  285. if (!$result) {
  286. throw new Exception('签名出错' . $plainText);
  287. }
  288. return base64_encode($sign);
  289. } catch (Exception $e) {
  290. throw $e;
  291. }
  292. }
  293. /**
  294. * 公钥验签
  295. * @param $plainText
  296. * @param $sign
  297. * @param $path
  298. * @return int
  299. * @throws Exception
  300. */
  301. function verify($plainText, $sign, $path)
  302. {
  303. $resource = openssl_pkey_get_public($path);
  304. $result = openssl_verify($plainText, base64_decode($sign), $resource);
  305. openssl_free_key($resource);
  306. if (!$result) {
  307. throw new Exception('签名验证未通过,plainText:' . $plainText . '。sign:' . $sign, '02002');
  308. }
  309. return $result;
  310. }
  311. /**
  312. * 公钥加密AESKey
  313. * @param $plainText
  314. * @param $puk
  315. * @return string
  316. * @throws Exception
  317. */
  318. function RSAEncryptByPub($plainText, $puk)
  319. {
  320. if (!openssl_public_encrypt($plainText, $cipherText, $puk, OPENSSL_PKCS1_PADDING)) {
  321. throw new Exception('AESKey 加密错误');
  322. }
  323. return base64_encode($cipherText);
  324. }
  325. /**
  326. * 私钥解密AESKey
  327. * @param $cipherText
  328. * @param $prk
  329. * @return string
  330. * @throws Exception
  331. */
  332. function RSADecryptByPri($cipherText, $prk)
  333. {
  334. if (!openssl_private_decrypt(base64_decode($cipherText), $plainText, $prk, OPENSSL_PKCS1_PADDING)) {
  335. throw new Exception('AESKey 解密错误');
  336. }
  337. return (string) $plainText;
  338. }
  339. /**
  340. * AES加密
  341. * @param $plainText
  342. * @param $key
  343. * @return string
  344. * @throws Exception
  345. */
  346. function AESEncrypt($plainText, $key)
  347. {
  348. $plainText = json_encode($plainText);
  349. $result = openssl_encrypt($plainText, 'AES-128-ECB', $key, 1);
  350. if (!$result) {
  351. throw new Exception('报文加密错误');
  352. }
  353. return base64_encode($result);
  354. }
  355. /**
  356. * AES解密
  357. * @param $cipherText
  358. * @param $key
  359. * @return string
  360. * @throws Exception
  361. */
  362. function AESDecrypt($cipherText, $key)
  363. {
  364. $result = openssl_decrypt(base64_decode($cipherText), 'AES-128-ECB', $key, 1);
  365. if (!$result) {
  366. throw new Exception('报文解密错误', 2003);
  367. }
  368. return $result;
  369. }
  370. /**
  371. * 生成AESKey
  372. * @param $size
  373. * @return string
  374. */
  375. function aes_generate($size)
  376. {
  377. $str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  378. $arr = array();
  379. for ($i = 0; $i < $size; $i++) {
  380. $arr[] = $str[mt_rand(0, 61)];
  381. }
  382. return implode('', $arr);
  383. }
  384. }