BasicWePay.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | WeChatDeveloper
  4. // +----------------------------------------------------------------------
  5. // | 版权所有 2014~2023 ThinkAdmin [ thinkadmin.top ]
  6. // +----------------------------------------------------------------------
  7. // | 官方网站: https://thinkadmin.top
  8. // +----------------------------------------------------------------------
  9. // | 开源协议 ( https://mit-license.org )
  10. // | 免责声明 ( https://thinkadmin.top/disclaimer )
  11. // +----------------------------------------------------------------------
  12. // | gitee 代码仓库:https://gitee.com/zoujingli/WeChatDeveloper
  13. // | github 代码仓库:https://github.com/zoujingli/WeChatDeveloper
  14. // +----------------------------------------------------------------------
  15. namespace WeChat\Contracts;
  16. use WeChat\Exceptions\InvalidArgumentException;
  17. use WeChat\Exceptions\InvalidResponseException;
  18. /**
  19. * 微信支付基础类
  20. * Class BasicPay
  21. * @package WeChat\Contracts
  22. */
  23. class BasicWePay
  24. {
  25. /**
  26. * 商户配置
  27. * @var DataArray
  28. */
  29. protected $config;
  30. /**
  31. * 当前请求数据
  32. * @var DataArray
  33. */
  34. protected $params;
  35. /**
  36. * 静态缓存
  37. * @var static
  38. */
  39. protected static $cache;
  40. /**
  41. * WeChat constructor.
  42. * @param array $options
  43. */
  44. public function __construct(array $options)
  45. {
  46. if (empty($options['appid'])) {
  47. throw new InvalidArgumentException("Missing Config -- [appid]");
  48. }
  49. if (empty($options['mch_id'])) {
  50. throw new InvalidArgumentException("Missing Config -- [mch_id]");
  51. }
  52. if (empty($options['mch_key'])) {
  53. throw new InvalidArgumentException("Missing Config -- [mch_key]");
  54. }
  55. if (!empty($options['cache_path'])) {
  56. Tools::$cache_path = $options['cache_path'];
  57. }
  58. $this->config = new DataArray($options);
  59. // 商户基础参数
  60. $this->params = new DataArray([
  61. 'appid' => $this->config->get('appid'),
  62. 'mch_id' => $this->config->get('mch_id'),
  63. 'nonce_str' => Tools::createNoncestr(),
  64. ]);
  65. // 商户参数支持
  66. if ($this->config->get('sub_appid')) {
  67. $this->params->set('sub_appid', $this->config->get('sub_appid'));
  68. }
  69. if ($this->config->get('sub_mch_id')) {
  70. $this->params->set('sub_mch_id', $this->config->get('sub_mch_id'));
  71. }
  72. }
  73. /**
  74. * 静态创建对象
  75. * @param array $config
  76. * @return static
  77. */
  78. public static function instance(array $config)
  79. {
  80. $key = md5(get_called_class() . serialize($config));
  81. if (isset(self::$cache[$key])) return self::$cache[$key];
  82. return self::$cache[$key] = new static($config);
  83. }
  84. /**
  85. * 获取微信支付通知
  86. * @param string $xml
  87. * @return array
  88. * @throws \WeChat\Exceptions\InvalidResponseException
  89. */
  90. public function getNotify($xml = '')
  91. {
  92. $data = Tools::xml2arr(empty($xml) ? Tools::getRawInput() : $xml);
  93. if (isset($data['sign']) && $this->getPaySign($data) === $data['sign']) {
  94. return $data;
  95. }
  96. throw new InvalidResponseException('Invalid Notify.', '0');
  97. }
  98. /**
  99. * 获取微信支付通知回复内容
  100. * @return string
  101. */
  102. public function getNotifySuccessReply()
  103. {
  104. return Tools::arr2xml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']);
  105. }
  106. /**
  107. * 生成支付签名
  108. * @param array $data 参与签名的数据
  109. * @param string $signType 参与签名的类型
  110. * @param string $buff 参与签名字符串前缀
  111. * @return string
  112. */
  113. public function getPaySign(array $data, $signType = 'MD5', $buff = '')
  114. {
  115. ksort($data);
  116. if (isset($data['sign'])) unset($data['sign']);
  117. foreach ($data as $k => $v) {
  118. if ('' === $v || null === $v) continue;
  119. $buff .= "{$k}={$v}&";
  120. }
  121. $buff .= ("key=" . $this->config->get('mch_key'));
  122. if (strtoupper($signType) === 'MD5') {
  123. return strtoupper(md5($buff));
  124. }
  125. return strtoupper(hash_hmac('SHA256', $buff, $this->config->get('mch_key')));
  126. }
  127. /**
  128. * 转换短链接
  129. * @param string $longUrl 需要转换的URL,签名用原串,传输需URLencode
  130. * @return array
  131. * @throws \WeChat\Exceptions\InvalidResponseException
  132. * @throws \WeChat\Exceptions\LocalCacheException
  133. */
  134. public function shortUrl($longUrl)
  135. {
  136. $url = 'https://api.mch.weixin.qq.com/tools/shorturl';
  137. return $this->callPostApi($url, ['long_url' => $longUrl]);
  138. }
  139. /**
  140. * 数组直接转xml数据输出
  141. * @param array $data
  142. * @param bool $isReturn
  143. * @return string
  144. */
  145. public function toXml(array $data, $isReturn = false)
  146. {
  147. $xml = Tools::arr2xml($data);
  148. if ($isReturn) {
  149. return $xml;
  150. }
  151. echo $xml;
  152. }
  153. /**
  154. * 以 Post 请求接口
  155. * @param string $url 请求
  156. * @param array $data 接口参数
  157. * @param bool $isCert 是否需要使用双向证书
  158. * @param string $signType 数据签名类型 MD5|SHA256
  159. * @param bool $needSignType 是否需要传签名类型参数
  160. * @param bool $needNonceStr
  161. * @return array
  162. * @throws \WeChat\Exceptions\InvalidResponseException
  163. * @throws \WeChat\Exceptions\LocalCacheException
  164. */
  165. protected function callPostApi($url, array $data, $isCert = false, $signType = 'HMAC-SHA256', $needSignType = true, $needNonceStr = true)
  166. {
  167. $option = [];
  168. if ($isCert) {
  169. $option['ssl_p12'] = $this->config->get('ssl_p12');
  170. $option['ssl_cer'] = $this->config->get('ssl_cer');
  171. $option['ssl_key'] = $this->config->get('ssl_key');
  172. if (is_string($option['ssl_p12']) && file_exists($option['ssl_p12'])) {
  173. $content = file_get_contents($option['ssl_p12']);
  174. if (openssl_pkcs12_read($content, $certs, $this->config->get('mch_id'))) {
  175. $option['ssl_key'] = Tools::pushFile(md5($certs['pkey']) . '.pem', $certs['pkey']);
  176. $option['ssl_cer'] = Tools::pushFile(md5($certs['cert']) . '.pem', $certs['cert']);
  177. } else throw new InvalidArgumentException("P12 certificate does not match MCH_ID --- ssl_p12");
  178. }
  179. if (empty($option['ssl_cer']) || !file_exists($option['ssl_cer'])) {
  180. throw new InvalidArgumentException("Missing Config -- ssl_cer", '0');
  181. }
  182. if (empty($option['ssl_key']) || !file_exists($option['ssl_key'])) {
  183. throw new InvalidArgumentException("Missing Config -- ssl_key", '0');
  184. }
  185. }
  186. $params = $this->params->merge($data);
  187. if (!$needNonceStr) unset($params['nonce_str']);
  188. if ($needSignType) $params['sign_type'] = strtoupper($signType);
  189. $params['sign'] = $this->getPaySign($params, $signType);
  190. $result = Tools::xml2arr(Tools::post($url, Tools::arr2xml($params), $option));
  191. if ($result['return_code'] !== 'SUCCESS') {
  192. throw new InvalidResponseException($result['return_msg'], '0');
  193. }
  194. return $result;
  195. }
  196. }