Wechat.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. <?php
  2. namespace Yansongda\Pay\Gateways;
  3. use Exception;
  4. use Symfony\Component\HttpFoundation\RedirectResponse;
  5. use Symfony\Component\HttpFoundation\Request;
  6. use Symfony\Component\HttpFoundation\Response;
  7. use Yansongda\Pay\Contracts\GatewayApplicationInterface;
  8. use Yansongda\Pay\Contracts\GatewayInterface;
  9. use Yansongda\Pay\Events;
  10. use Yansongda\Pay\Exceptions\GatewayException;
  11. use Yansongda\Pay\Exceptions\InvalidArgumentException;
  12. use Yansongda\Pay\Exceptions\InvalidGatewayException;
  13. use Yansongda\Pay\Exceptions\InvalidSignException;
  14. use Yansongda\Pay\Gateways\Wechat\Support;
  15. use Yansongda\Pay\Log;
  16. use Yansongda\Supports\Collection;
  17. use Yansongda\Supports\Config;
  18. use Yansongda\Supports\Str;
  19. /**
  20. * @method Response app(array $config) APP 支付
  21. * @method Collection groupRedpack(array $config) 分裂红包
  22. * @method Collection miniapp(array $config) 小程序支付
  23. * @method Collection mp(array $config) 公众号支付
  24. * @method Collection pos(array $config) 刷卡支付
  25. * @method Collection redpack(array $config) 普通红包
  26. * @method Collection scan(array $config) 扫码支付
  27. * @method Collection transfer(array $config) 企业付款
  28. * @method RedirectResponse web(array $config) Web 扫码支付
  29. * @method RedirectResponse wap(array $config) H5 支付
  30. */
  31. class Wechat implements GatewayApplicationInterface
  32. {
  33. /**
  34. * 普通模式.
  35. */
  36. const MODE_NORMAL = 'normal';
  37. /**
  38. * 沙箱模式.
  39. */
  40. const MODE_DEV = 'dev';
  41. /**
  42. * 香港钱包 API.
  43. */
  44. const MODE_HK = 'hk';
  45. /**
  46. * 境外 API.
  47. */
  48. const MODE_US = 'us';
  49. /**
  50. * 服务商模式.
  51. */
  52. const MODE_SERVICE = 'service';
  53. /**
  54. * Const url.
  55. */
  56. const URL = [
  57. self::MODE_NORMAL => 'https://api.mch.weixin.qq.com/',
  58. self::MODE_DEV => 'https://api.mch.weixin.qq.com/xdc/apiv2sandbox/',
  59. self::MODE_HK => 'https://apihk.mch.weixin.qq.com/',
  60. self::MODE_SERVICE => 'https://api.mch.weixin.qq.com/',
  61. self::MODE_US => 'https://apius.mch.weixin.qq.com/',
  62. ];
  63. /**
  64. * Wechat payload.
  65. *
  66. * @var array
  67. */
  68. protected $payload;
  69. /**
  70. * Wechat gateway.
  71. *
  72. * @var string
  73. */
  74. protected $gateway;
  75. /**
  76. * Bootstrap.
  77. *
  78. * @author yansongda <me@yansongda.cn>
  79. *
  80. * @throws Exception
  81. */
  82. public function __construct(Config $config)
  83. {
  84. $this->gateway = Support::create($config)->getBaseUri();
  85. $this->payload = [
  86. 'appid' => $config->get('app_id', ''),
  87. 'mch_id' => $config->get('mch_id', ''),
  88. 'nonce_str' => Str::random(),
  89. 'notify_url' => $config->get('notify_url', ''),
  90. 'sign' => '',
  91. 'trade_type' => '',
  92. 'spbill_create_ip' => Request::createFromGlobals()->getClientIp(),
  93. ];
  94. if ($config->get('mode', self::MODE_NORMAL) === static::MODE_SERVICE) {
  95. $this->payload = array_merge($this->payload, [
  96. 'sub_mch_id' => $config->get('sub_mch_id'),
  97. 'sub_appid' => $config->get('sub_app_id', ''),
  98. ]);
  99. }
  100. }
  101. /**
  102. * Magic pay.
  103. *
  104. * @author yansongda <me@yansongda.cn>
  105. *
  106. * @param string $method
  107. * @param string $params
  108. *
  109. * @throws InvalidGatewayException
  110. *
  111. * @return Response|Collection
  112. */
  113. public function __call($method, $params)
  114. {
  115. return self::pay($method, ...$params);
  116. }
  117. /**
  118. * Pay an order.
  119. *
  120. * @author yansongda <me@yansongda.cn>
  121. *
  122. * @param string $gateway
  123. * @param array $params
  124. *
  125. * @throws InvalidGatewayException
  126. *
  127. * @return Response|Collection
  128. */
  129. public function pay($gateway, $params = [])
  130. {
  131. Events::dispatch(new Events\PayStarting('Wechat', $gateway, $params));
  132. $this->payload = array_merge($this->payload, $params);
  133. $gateway = get_class($this).'\\'.Str::studly($gateway).'Gateway';
  134. if (class_exists($gateway)) {
  135. return $this->makePay($gateway);
  136. }
  137. throw new InvalidGatewayException("Pay Gateway [{$gateway}] Not Exists");
  138. }
  139. /**
  140. * Verify data.
  141. *
  142. * @author yansongda <me@yansongda.cn>
  143. *
  144. * @param string|null $content
  145. *
  146. * @throws InvalidSignException
  147. * @throws InvalidArgumentException
  148. */
  149. public function verify($content = null, bool $refund = false): Collection
  150. {
  151. $content = $content ?? Request::createFromGlobals()->getContent();
  152. Events::dispatch(new Events\RequestReceived('Wechat', '', [$content]));
  153. $data = Support::fromXml($content);
  154. if ($refund) {
  155. $decrypt_data = Support::decryptRefundContents($data['req_info']);
  156. $data = array_merge(Support::fromXml($decrypt_data), $data);
  157. }
  158. Log::debug('Resolved The Received Wechat Request Data', $data);
  159. if ($refund || Support::generateSign($data) === $data['sign']) {
  160. return new Collection($data);
  161. }
  162. Events::dispatch(new Events\SignFailed('Wechat', '', $data));
  163. throw new InvalidSignException('Wechat Sign Verify FAILED', $data);
  164. }
  165. /**
  166. * Query an order.
  167. *
  168. * @author yansongda <me@yansongda.cn>
  169. *
  170. * @param string|array $order
  171. *
  172. * @throws GatewayException
  173. * @throws InvalidSignException
  174. * @throws InvalidArgumentException
  175. */
  176. public function find($order, string $type = 'wap'): Collection
  177. {
  178. // if ('wap' != $type) {
  179. unset($this->payload['spbill_create_ip']);
  180. // }
  181. $gateway = get_class($this).'\\'.Str::studly($type).'Gateway';
  182. if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) {
  183. throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method");
  184. }
  185. $config = call_user_func([new $gateway(), 'find'], $order);
  186. $this->payload = Support::filterPayload($this->payload, $config['order']);
  187. Events::dispatch(new Events\MethodCalled('Wechat', 'Find', $this->gateway, $this->payload));
  188. return Support::requestApi(
  189. $config['endpoint'],
  190. $this->payload,
  191. $config['cert']
  192. );
  193. }
  194. /**
  195. * Refund an order.
  196. *
  197. * @author yansongda <me@yansongda.cn>
  198. *
  199. * @throws GatewayException
  200. * @throws InvalidSignException
  201. * @throws InvalidArgumentException
  202. */
  203. public function refund(array $order): Collection
  204. {
  205. $this->payload = Support::filterPayload($this->payload, $order, true);
  206. Events::dispatch(new Events\MethodCalled('Wechat', 'Refund', $this->gateway, $this->payload));
  207. return Support::requestApi(
  208. 'secapi/pay/refund',
  209. $this->payload,
  210. true
  211. );
  212. }
  213. /**
  214. * Cancel an order.
  215. *
  216. * @author yansongda <me@yansongda.cn>
  217. *
  218. * @param array $order
  219. *
  220. * @throws GatewayException
  221. * @throws InvalidSignException
  222. * @throws InvalidArgumentException
  223. */
  224. public function cancel($order): Collection
  225. {
  226. unset($this->payload['spbill_create_ip']);
  227. $this->payload = Support::filterPayload($this->payload, $order);
  228. Events::dispatch(new Events\MethodCalled('Wechat', 'Cancel', $this->gateway, $this->payload));
  229. return Support::requestApi(
  230. 'secapi/pay/reverse',
  231. $this->payload,
  232. true
  233. );
  234. }
  235. /**
  236. * Close an order.
  237. *
  238. * @author yansongda <me@yansongda.cn>
  239. *
  240. * @param string|array $order
  241. *
  242. * @throws GatewayException
  243. * @throws InvalidSignException
  244. * @throws InvalidArgumentException
  245. */
  246. public function close($order): Collection
  247. {
  248. unset($this->payload['spbill_create_ip']);
  249. $this->payload = Support::filterPayload($this->payload, $order);
  250. Events::dispatch(new Events\MethodCalled('Wechat', 'Close', $this->gateway, $this->payload));
  251. return Support::requestApi('pay/closeorder', $this->payload);
  252. }
  253. /**
  254. * Echo success to server.
  255. *
  256. * @author yansongda <me@yansongda.cn>
  257. *
  258. * @throws InvalidArgumentException
  259. */
  260. public function success(): Response
  261. {
  262. Events::dispatch(new Events\MethodCalled('Wechat', 'Success', $this->gateway));
  263. return new Response(
  264. Support::toXml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']),
  265. 200,
  266. ['Content-Type' => 'application/xml']
  267. );
  268. }
  269. /**
  270. * Download the bill.
  271. *
  272. * @author yansongda <me@yansongda.cn>
  273. *
  274. * @throws GatewayException
  275. * @throws InvalidArgumentException
  276. */
  277. public function download(array $params): string
  278. {
  279. unset($this->payload['spbill_create_ip']);
  280. $this->payload = Support::filterPayload($this->payload, $params, true);
  281. Events::dispatch(new Events\MethodCalled('Wechat', 'Download', $this->gateway, $this->payload));
  282. $result = Support::getInstance()->post(
  283. 'pay/downloadbill',
  284. Support::getInstance()->toXml($this->payload)
  285. );
  286. if (is_array($result)) {
  287. throw new GatewayException('Get Wechat API Error: '.$result['return_msg'], $result);
  288. }
  289. return $result;
  290. }
  291. /**
  292. * Make pay gateway.
  293. *
  294. * @author yansongda <me@yansongda.cn>
  295. *
  296. * @param string $gateway
  297. *
  298. * @throws InvalidGatewayException
  299. *
  300. * @return Response|Collection
  301. */
  302. protected function makePay($gateway)
  303. {
  304. $app = new $gateway();
  305. if ($app instanceof GatewayInterface) {
  306. return $app->pay($this->gateway, array_filter($this->payload, function ($value) {
  307. return '' !== $value && !is_null($value);
  308. }));
  309. }
  310. throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface");
  311. }
  312. }