国内流行的内容管理系统(CMS)多端全媒体解决方案 https://www.dedebiz.com
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.

332 lines
12KB

  1. <?php
  2. namespace WeChat\Contracts;
  3. if (!defined('DEDEINC')) exit ('dedebiz');
  4. use WeChat\Exceptions\InvalidArgumentException;
  5. use WeChat\Exceptions\InvalidResponseException;
  6. /**
  7. * 支付宝支付基类
  8. * Class AliPay
  9. * @package AliPay\Contracts
  10. */
  11. abstract class BasicAliPay
  12. {
  13. /**
  14. * 支持配置
  15. * @var DataArray
  16. */
  17. protected $config;
  18. /**
  19. * 当前请求数据
  20. * @var DataArray
  21. */
  22. protected $options;
  23. /**
  24. * DzContent数据
  25. * @var DataArray
  26. */
  27. protected $params;
  28. /**
  29. * 静态缓存
  30. * @var static
  31. */
  32. protected static $cache;
  33. /**
  34. * 正常请求网关
  35. * @var string
  36. */
  37. protected $gateway = 'https://openapi.alipay.com/gateway.do?charset=utf-8';
  38. /**
  39. * AliPay constructor.
  40. * @param array $options
  41. */
  42. public function __construct($options)
  43. {
  44. $this->params = new DataArray([]);
  45. $this->config = new DataArray($options);
  46. if (empty($options['appid'])) {
  47. throw new InvalidArgumentException("Missing Config -- [appid]");
  48. }
  49. if (empty($options['public_key'])) {
  50. throw new InvalidArgumentException("Missing Config -- [public_key]");
  51. }
  52. if (empty($options['private_key'])) {
  53. throw new InvalidArgumentException("Missing Config -- [private_key]");
  54. }
  55. if (!empty($options['debug'])) {
  56. $this->gateway = 'https://openapi.alipaydev.com/gateway.do?charset=utf-8';
  57. }
  58. $this->options = new DataArray([
  59. 'app_id' => $this->config->get('appid'),
  60. 'charset' => empty($options['charset']) ? 'utf-8' : $options['charset'],
  61. 'format' => 'JSON',
  62. 'version' => '1.0',
  63. 'sign_type' => empty($options['sign_type']) ? 'RSA2' : $options['sign_type'],
  64. 'timestamp' => date('Y-m-d H:i:s'),
  65. ]);
  66. if (isset($options['notify_url']) && $options['notify_url'] !== '') {
  67. $this->options->set('notify_url', $options['notify_url']);
  68. }
  69. if (isset($options['return_url']) && $options['return_url'] !== '') {
  70. $this->options->set('return_url', $options['return_url']);
  71. }
  72. if (isset($options['app_auth_token']) && $options['app_auth_token'] !== '') {
  73. $this->options->set('app_auth_token', $options['app_auth_token']);
  74. }
  75. }
  76. /**
  77. * 静态创建对象
  78. * @param array $config
  79. * @return static
  80. */
  81. public static function instance(array $config)
  82. {
  83. $key = md5(get_called_class().serialize($config));
  84. if (isset(self::$cache[$key])) return self::$cache[$key];
  85. return self::$cache[$key] = new static($config);
  86. }
  87. /**
  88. * 查询支付宝订单状态
  89. * @param string $out_trade_no
  90. * @return array|boolean
  91. * @throws \WeChat\Exceptions\InvalidResponseException
  92. * @throws \WeChat\Exceptions\LocalCacheException
  93. */
  94. public function query($out_trade_no = '')
  95. {
  96. $this->options->set('method', 'alipay.trade.query');
  97. return $this->getResult(['out_trade_no' => $out_trade_no]);
  98. }
  99. /**
  100. * 支付宝订单退款操作
  101. * @param array|string $options 退款参数或退款商户订单号
  102. * @param null $refund_amount 退款金额
  103. * @return array|boolean
  104. * @throws \WeChat\Exceptions\InvalidResponseException
  105. * @throws \WeChat\Exceptions\LocalCacheException
  106. */
  107. public function refund($options, $refund_amount = null)
  108. {
  109. if (!is_array($options)) $options = ['out_trade_no' => $options, 'refund_amount' => $refund_amount];
  110. $this->options->set('method', 'alipay.trade.refund');
  111. return $this->getResult($options);
  112. }
  113. /**
  114. * 关闭支付宝进行中的订单
  115. * @param array|string $options
  116. * @return array|boolean
  117. * @throws \WeChat\Exceptions\InvalidResponseException
  118. * @throws \WeChat\Exceptions\LocalCacheException
  119. */
  120. public function close($options)
  121. {
  122. if (!is_array($options)) $options = ['out_trade_no' => $options];
  123. $this->options->set('method', 'alipay.trade.close');
  124. return $this->getResult($options);
  125. }
  126. /**
  127. * 获取通知数据
  128. *
  129. * @param boolean $needSignType 是否需要sign_type字段
  130. * @param array $parameters
  131. * @return array
  132. * @throws \WeChat\Exceptions\InvalidResponseException
  133. */
  134. public function notify($needSignType = false, array $parameters = [])
  135. {
  136. $data = empty($parameters) ? $_POST : $parameters;
  137. if (empty($data) || empty($data['sign'])) {
  138. throw new InvalidResponseException('Illegal push request.', 0, $data);
  139. }
  140. $string = $this->getSignContent($data, $needSignType);
  141. $content = wordwrap($this->config->get('public_key'), 64, "\n", true);
  142. $res = "-----BEGIN PUBLIC KEY-----\n{$content}\n-----END PUBLIC KEY-----";
  143. if (openssl_verify($string, base64_decode($data['sign']), $res, OPENSSL_ALGO_SHA256) !== 1) {
  144. throw new InvalidResponseException('Data signature verification failed.', 0, $data);
  145. }
  146. return $data;
  147. }
  148. /**
  149. * 验证接口返回的数据签名
  150. * @param array $data 通知数据
  151. * @param null|string $sign 数据签名
  152. * @return array
  153. * @throws \WeChat\Exceptions\InvalidResponseException
  154. */
  155. protected function verify($data, $sign)
  156. {
  157. $content = wordwrap($this->config->get('public_key'), 64, "\n", true);
  158. $res = "-----BEGIN PUBLIC KEY-----\n{$content}\n-----END PUBLIC KEY-----";
  159. if ($this->options->get('sign_type') === 'RSA2') {
  160. if (openssl_verify(json_encode($data, 256), base64_decode($sign), $res, OPENSSL_ALGO_SHA256) !== 1) {
  161. throw new InvalidResponseException('Data signature verification failed.');
  162. }
  163. } else {
  164. if (openssl_verify(json_encode($data, 256), base64_decode($sign), $res, OPENSSL_ALGO_SHA1) !== 1) {
  165. throw new InvalidResponseException('Data signature verification failed.');
  166. }
  167. }
  168. return $data;
  169. }
  170. /**
  171. * 获取数据签名
  172. * @return string
  173. */
  174. protected function getSign()
  175. {
  176. $content = wordwrap($this->trimCert($this->config->get('private_key')), 64, "\n", true);
  177. $string = "-----BEGIN RSA PRIVATE KEY-----\n{$content}\n-----END RSA PRIVATE KEY-----";
  178. if ($this->options->get('sign_type') === 'RSA2') {
  179. openssl_sign($this->getSignContent($this->options->get(), true), $sign, $string, OPENSSL_ALGO_SHA256);
  180. } else {
  181. openssl_sign($this->getSignContent($this->options->get(), true), $sign, $string, OPENSSL_ALGO_SHA1);
  182. }
  183. return base64_encode($sign);
  184. }
  185. /**
  186. * 去除证书前后内容及空白
  187. * @param string $sign
  188. * @return string
  189. */
  190. protected function trimCert($sign)
  191. {
  192. //if (file_exists($sign)) $sign = file_get_contents($sign);
  193. return preg_replace(['/\s+/', '/\-{5}.*?\-{5}/'], '', $sign);
  194. }
  195. /**
  196. * 数据签名处理
  197. * @param array $data 需要进行签名数据
  198. * @param boolean $needSignType 是否需要sign_type字段
  199. * @return string
  200. */
  201. private function getSignContent(array $data, $needSignType = false)
  202. {
  203. list($attrs,) = [[], ksort($data)];
  204. if (isset($data['sign'])) unset($data['sign']);
  205. if (empty($needSignType)) unset($data['sign_type']);
  206. foreach ($data as $key => $value) {
  207. if ($value === '' || is_null($value)) continue;
  208. $attrs[] = "{$key}={$value}";
  209. }
  210. return join('&', $attrs);
  211. }
  212. /**
  213. * 数据包生成及数据签名
  214. * @param array $options
  215. */
  216. protected function applyData($options)
  217. {
  218. $this->options->set('biz_content', json_encode($this->params->merge($options), 256));
  219. $this->options->set('sign', $this->getSign());
  220. }
  221. /**
  222. * 请求接口并验证访问数据
  223. * @param array $options
  224. * @return array|boolean
  225. * @throws \WeChat\Exceptions\InvalidResponseException
  226. * @throws \WeChat\Exceptions\LocalCacheException
  227. */
  228. protected function getResult($options)
  229. {
  230. $this->applyData($options);
  231. $method = str_replace('.', '_', $this->options['method']).'_response';
  232. $data = json_decode(Tools::get($this->gateway, $this->options->get()), true);
  233. if (!isset($data[$method]['code']) || $data[$method]['code'] !== '10000') {
  234. throw new InvalidResponseException(
  235. "Error: " .
  236. (empty($data[$method]['code']) ? '' : "{$data[$method]['msg']} [{$data[$method]['code']}]\r\n") .
  237. (empty($data[$method]['sub_code']) ? '' : "{$data[$method]['sub_msg']} [{$data[$method]['sub_code']}]\r\n"),
  238. $data[$method]['code'], $data
  239. );
  240. }
  241. return $data[$method];
  242. //去除返回结果签名检查
  243. //return $this->verify($data[$method], $data['sign']);
  244. }
  245. /**
  246. * 生成支付HTML代码
  247. * @return string
  248. */
  249. protected function buildPayHtml()
  250. {
  251. $html = "<form id='alipaysubmit' name='alipaysubmit' action='{$this->gateway}' method='post'>";
  252. foreach ($this->options->get() as $key => $value) {
  253. $value = str_replace("'", '&apos;', $value);
  254. $html .= "<input type='hidden' name='{$key}' value='{$value}'/>";
  255. }
  256. $html .= "<input type='submit' value='ok' style='display:none;'></form>";
  257. return "{$html}<script>document.forms['alipaysubmit'].submit();</script>";
  258. }
  259. /**
  260. * 新版 从证书中提取序列号
  261. * @param string $sign
  262. * @return string
  263. */
  264. public function getCertSN($sign)
  265. {
  266. //if (file_exists($sign)) $sign = file_get_contents($sign);
  267. $ssl = openssl_x509_parse($sign);
  268. return md5($this->_arr2str(array_reverse($ssl['issuer'])).$ssl['serialNumber']);
  269. }
  270. /**
  271. * 新版 提取根证书序列号
  272. * @param string $sign
  273. * @return string|null
  274. */
  275. public function getRootCertSN($sign)
  276. {
  277. $sn = null;
  278. //if (file_exists($sign)) $sign = file_get_contents($sign);
  279. $array = explode("-----END CERTIFICATE-----", $sign);
  280. for ($i = 0; $i < count($array) - 1; $i++) {
  281. $ssl[$i] = openssl_x509_parse($array[$i]."-----END CERTIFICATE-----");
  282. if (strpos($ssl[$i]['serialNumber'], '0x') === 0) {
  283. $ssl[$i]['serialNumber'] = $this->_hex2dec($ssl[$i]['serialNumber']);
  284. }
  285. if ($ssl[$i]['signatureTypeLN'] == "sha1WithRSAEncryption" || $ssl[$i]['signatureTypeLN'] == "sha256WithRSAEncryption") {
  286. if ($sn == null) {
  287. $sn = md5($this->_arr2str(array_reverse($ssl[$i]['issuer'])).$ssl[$i]['serialNumber']);
  288. } else {
  289. $sn = $sn."_".md5($this->_arr2str(array_reverse($ssl[$i]['issuer'])).$ssl[$i]['serialNumber']);
  290. }
  291. }
  292. }
  293. return $sn;
  294. }
  295. /**
  296. * 新版 数组转字符串
  297. * @param array $array
  298. * @return string
  299. */
  300. private function _arr2str($array)
  301. {
  302. $string = [];
  303. if ($array && is_array($array)) {
  304. foreach ($array as $key => $value) {
  305. $string[] = $key.'='.$value;
  306. }
  307. }
  308. return implode(',', $string);
  309. }
  310. /**
  311. * 新版 0x转高精度数字
  312. * @param string $hex
  313. * @return int|string
  314. */
  315. private function _hex2dec($hex)
  316. {
  317. list($dec, $len) = [0, strlen($hex)];
  318. for ($i = 1; $i <= $len; $i++) {
  319. $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
  320. }
  321. return $dec;
  322. }
  323. /**
  324. * 应用数据操作
  325. * @param array $options
  326. * @return mixed
  327. */
  328. abstract public function apply($options);
  329. }
  330. ?>