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. unset($this->payload['spbill_create_ip']);
  179. $gateway = get_class($this).'\\'.Str::studly($type).'Gateway';
  180. if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) {
  181. throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method");
  182. }
  183. $config = call_user_func([new $gateway(), 'find'], $order);
  184. $this->payload = Support::filterPayload($this->payload, $config['order']);
  185. Events::dispatch(new Events\MethodCalled('Wechat', 'Find', $this->gateway, $this->payload));
  186. return Support::requestApi(
  187. $config['endpoint'],
  188. $this->payload,
  189. $config['cert']
  190. );
  191. }
  192. /**
  193. * Refund an order.
  194. *
  195. * @author yansongda <me@yansongda.cn>
  196. *
  197. * @throws GatewayException
  198. * @throws InvalidSignException
  199. * @throws InvalidArgumentException
  200. */
  201. public function refund(array $order): Collection
  202. {
  203. unset($this->payload['spbill_create_ip']);
  204. $this->payload = Support::filterPayload($this->payload, $order, true);
  205. Events::dispatch(new Events\MethodCalled('Wechat', 'Refund', $this->gateway, $this->payload));
  206. return Support::requestApi(
  207. 'secapi/pay/refund',
  208. $this->payload,
  209. true
  210. );
  211. }
  212. /**
  213. * Cancel an order.
  214. *
  215. * @author yansongda <me@yansongda.cn>
  216. *
  217. * @param array $order
  218. *
  219. * @throws GatewayException
  220. * @throws InvalidSignException
  221. * @throws InvalidArgumentException
  222. */
  223. public function cancel($order): Collection
  224. {
  225. unset($this->payload['spbill_create_ip']);
  226. $this->payload = Support::filterPayload($this->payload, $order);
  227. Events::dispatch(new Events\MethodCalled('Wechat', 'Cancel', $this->gateway, $this->payload));
  228. return Support::requestApi(
  229. 'secapi/pay/reverse',
  230. $this->payload,
  231. true
  232. );
  233. }
  234. /**
  235. * Close an order.
  236. *
  237. * @author yansongda <me@yansongda.cn>
  238. *
  239. * @param string|array $order
  240. *
  241. * @throws GatewayException
  242. * @throws InvalidSignException
  243. * @throws InvalidArgumentException
  244. */
  245. public function close($order): Collection
  246. {
  247. unset($this->payload['spbill_create_ip']);
  248. $this->payload = Support::filterPayload($this->payload, $order);
  249. Events::dispatch(new Events\MethodCalled('Wechat', 'Close', $this->gateway, $this->payload));
  250. return Support::requestApi('pay/closeorder', $this->payload);
  251. }
  252. /**
  253. * Echo success to server.
  254. *
  255. * @author yansongda <me@yansongda.cn>
  256. *
  257. * @throws InvalidArgumentException
  258. */
  259. public function success(): Response
  260. {
  261. Events::dispatch(new Events\MethodCalled('Wechat', 'Success', $this->gateway));
  262. return new Response(
  263. Support::toXml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']),
  264. 200,
  265. ['Content-Type' => 'application/xml']
  266. );
  267. }
  268. /**
  269. * Download the bill.
  270. *
  271. * @author yansongda <me@yansongda.cn>
  272. *
  273. * @throws GatewayException
  274. * @throws InvalidArgumentException
  275. */
  276. public function download(array $params): string
  277. {
  278. unset($this->payload['spbill_create_ip']);
  279. $this->payload = Support::filterPayload($this->payload, $params, true);
  280. Events::dispatch(new Events\MethodCalled('Wechat', 'Download', $this->gateway, $this->payload));
  281. $result = Support::getInstance()->post(
  282. 'pay/downloadbill',
  283. Support::getInstance()->toXml($this->payload)
  284. );
  285. if (is_array($result)) {
  286. throw new GatewayException('Get Wechat API Error: '.$result['return_msg'], $result);
  287. }
  288. return $result;
  289. }
  290. /**
  291. * Make pay gateway.
  292. *
  293. * @author yansongda <me@yansongda.cn>
  294. *
  295. * @param string $gateway
  296. *
  297. * @throws InvalidGatewayException
  298. *
  299. * @return Response|Collection
  300. */
  301. protected function makePay($gateway)
  302. {
  303. $app = new $gateway();
  304. if ($app instanceof GatewayInterface) {
  305. return $app->pay($this->gateway, array_filter($this->payload, function ($value) {
  306. return '' !== $value && !is_null($value);
  307. }));
  308. }
  309. throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface");
  310. }
  311. }