Wechat.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <?php
  2. namespace Yansongda\Pay\Gateways\Wechat;
  3. use Yansongda\Pay\Contracts\GatewayInterface;
  4. use Yansongda\Pay\Exceptions\GatewayException;
  5. use Yansongda\Pay\Exceptions\InvalidArgumentException;
  6. use Yansongda\Pay\Support\Config;
  7. use Yansongda\Pay\Traits\HasHttpRequest;
  8. abstract class Wechat implements GatewayInterface
  9. {
  10. use HasHttpRequest;
  11. /**
  12. * @var string
  13. */
  14. protected $endpoint = 'https://api.mch.weixin.qq.com/';
  15. /**
  16. * @var string
  17. */
  18. protected $gateway_order = 'pay/unifiedorder';
  19. /**
  20. * @var string
  21. */
  22. protected $gateway_query = 'pay/orderquery';
  23. /**
  24. * @var string
  25. */
  26. protected $gateway_close = 'pay/closeorder';
  27. /**
  28. * @var string
  29. */
  30. protected $gateway_refund = 'secapi/pay/refund';
  31. /**
  32. * @var array
  33. */
  34. protected $config;
  35. /**
  36. * @var \Yansongda\Pay\Support\Config
  37. */
  38. protected $user_config;
  39. /**
  40. * [__construct description].
  41. *
  42. * @author yansongda <me@yansongda.cn>
  43. *
  44. * @param array $config
  45. */
  46. public function __construct(array $config)
  47. {
  48. $this->user_config = new Config($config);
  49. $this->config = [
  50. 'appid' => $this->user_config->get('app_id', ''),
  51. 'mch_id' => $this->user_config->get('mch_id', ''),
  52. 'nonce_str' => $this->createNonceStr(),
  53. 'sign_type' => 'MD5',
  54. 'notify_url' => $this->user_config->get('notify_url', ''),
  55. 'trade_type' => $this->getTradeType(),
  56. ];
  57. if ($endpoint = $this->user_config->get('endpoint_url')) {
  58. $this->endpoint = $endpoint;
  59. }
  60. }
  61. /**
  62. * pay a order.
  63. *
  64. * @author yansongda <me@yansongda.cn>
  65. *
  66. * @param array $config_biz
  67. *
  68. * @return mixed
  69. */
  70. abstract public function pay(array $config_biz = []);
  71. /**
  72. * refund.
  73. *
  74. * @author yansongda <me@yansongda.cn>
  75. *
  76. * @return string|bool
  77. */
  78. public function refund($config_biz = [])
  79. {
  80. if (isset($config_biz['miniapp'])) {
  81. $this->config['appid'] = $this->user_config->get('miniapp_id');
  82. unset($config_biz['miniapp']);
  83. }
  84. $this->config = array_merge($this->config, $config_biz);
  85. $this->unsetTradeTypeAndNotifyUrl();
  86. return $this->getResult($this->gateway_refund, true);
  87. }
  88. /**
  89. * close a order.
  90. *
  91. * @author yansongda <me@yansongda.cn>
  92. *
  93. * @return array|bool
  94. */
  95. public function close($out_trade_no = '')
  96. {
  97. $this->config['out_trade_no'] = $out_trade_no;
  98. $this->unsetTradeTypeAndNotifyUrl();
  99. return $this->getResult($this->gateway_close);
  100. }
  101. /**
  102. * find a order.
  103. *
  104. * @author yansongda <me@yansongda.cn>
  105. *
  106. * @param string $out_trade_no
  107. *
  108. * @return array|bool
  109. */
  110. public function find($out_trade_no = '')
  111. {
  112. $this->config['out_trade_no'] = $out_trade_no;
  113. $this->unsetTradeTypeAndNotifyUrl();
  114. return $this->getResult($this->gateway_query);
  115. }
  116. /**
  117. * verify the notify.
  118. *
  119. * @author yansongda <me@yansongda.cn>
  120. *
  121. * @param string $data
  122. * @param string $sign
  123. * @param bool $sync
  124. *
  125. * @return array|bool
  126. */
  127. public function verify($data, $sign = null, $sync = false)
  128. {
  129. $data = $this->fromXml($data);
  130. $sign = is_null($sign) ? $data['sign'] : $sign;
  131. return $this->getSign($data) === $sign ? $data : false;
  132. }
  133. /**
  134. * get trade type config.
  135. *
  136. * @author yansongda <me@yansongda.cn>
  137. *
  138. * @return string
  139. */
  140. abstract protected function getTradeType();
  141. /**
  142. * pre order.
  143. *
  144. * @author yansongda <me@yansongda.cn>
  145. *
  146. * @param array $config_biz
  147. *
  148. * @return array
  149. */
  150. protected function preOrder($config_biz = [])
  151. {
  152. $this->config = array_merge($this->config, $config_biz);
  153. return $this->getResult($this->gateway_order);
  154. }
  155. /**
  156. * get api result.
  157. *
  158. * @author yansongda <me@yansongda.cn>
  159. *
  160. * @param string $path
  161. * @param bool $cert
  162. *
  163. * @return array
  164. */
  165. protected function getResult($path, $cert = false)
  166. {
  167. $this->config['sign'] = $this->getSign($this->config);
  168. if ($cert) {
  169. $data = $this->fromXml($this->post(
  170. $this->endpoint.$path,
  171. $this->toXml($this->config),
  172. [
  173. 'cert' => $this->user_config->get('cert_client', ''),
  174. 'ssl_key' => $this->user_config->get('cert_key', ''),
  175. ]
  176. ));
  177. } else {
  178. $data = $this->fromXml($this->post($this->endpoint.$path, $this->toXml($this->config)));
  179. }
  180. if (!isset($data['return_code']) || $data['return_code'] !== 'SUCCESS' || $data['result_code'] !== 'SUCCESS') {
  181. $error = 'getResult error:'.$data['return_msg'];
  182. $error .= isset($data['err_code_des']) ? ' - '.$data['err_code_des'] : '';
  183. }
  184. if (!isset($error) && $this->getSign($data) !== $data['sign']) {
  185. $error = 'getResult error: return data sign error';
  186. }
  187. if (isset($error)) {
  188. throw new GatewayException(
  189. $error,
  190. 20000,
  191. $data);
  192. }
  193. return $data;
  194. }
  195. /**
  196. * sign.
  197. *
  198. * @author yansongda <me@yansongda.cn>
  199. *
  200. * @param array $data
  201. *
  202. * @return string
  203. */
  204. protected function getSign($data)
  205. {
  206. if (is_null($this->user_config->get('key'))) {
  207. throw new InvalidArgumentException('Missing Config -- [key]');
  208. }
  209. ksort($data);
  210. $string = md5($this->getSignContent($data).'&key='.$this->user_config->get('key'));
  211. return strtoupper($string);
  212. }
  213. /**
  214. * get sign content.
  215. *
  216. * @author yansongda <me@yansongda.cn>
  217. *
  218. * @param array $data
  219. *
  220. * @return string
  221. */
  222. protected function getSignContent($data)
  223. {
  224. $buff = '';
  225. foreach ($data as $k => $v) {
  226. $buff .= ($k != 'sign' && $v != '' && !is_array($v)) ? $k.'='.$v.'&' : '';
  227. }
  228. return trim($buff, '&');
  229. }
  230. /**
  231. * create random string.
  232. *
  233. * @author yansongda <me@yansongda.cn>
  234. *
  235. * @param int $length
  236. *
  237. * @return string
  238. */
  239. protected function createNonceStr($length = 16)
  240. {
  241. $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  242. $str = '';
  243. for ($i = 0; $i < $length; $i++) {
  244. $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
  245. }
  246. return $str;
  247. }
  248. /**
  249. * convert to xml.
  250. *
  251. * @author yansongda <me@yansongda.cn>
  252. *
  253. * @param array $data
  254. *
  255. * @return string
  256. */
  257. protected function toXml($data)
  258. {
  259. if (!is_array($data) || count($data) <= 0) {
  260. throw new InvalidArgumentException('convert to xml error!invalid array!');
  261. }
  262. $xml = '<xml>';
  263. foreach ($data as $key => $val) {
  264. $xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' :
  265. '<'.$key.'><![CDATA['.$val.']]></'.$key.'>';
  266. }
  267. $xml .= '</xml>';
  268. return $xml;
  269. }
  270. /**
  271. * convert to array.
  272. *
  273. * @author yansongda <me@yansongda.cn>
  274. *
  275. * @param string $xml
  276. *
  277. * @return array
  278. */
  279. protected function fromXml($xml)
  280. {
  281. if (!$xml) {
  282. throw new InvalidArgumentException('convert to array error !invalid xml');
  283. }
  284. libxml_disable_entity_loader(true);
  285. return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
  286. }
  287. /**
  288. * delete trade_type and notify_url.
  289. *
  290. * @author yansongda <me@yansongda.cn>
  291. *
  292. * @return bool
  293. */
  294. protected function unsetTradeTypeAndNotifyUrl()
  295. {
  296. unset($this->config['notify_url']);
  297. unset($this->config['trade_type']);
  298. return true;
  299. }
  300. }