WechatService.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. <?php
  2. /**
  3. *
  4. * @author: xaboy<365615158@qq.com>
  5. * @day: 2017/11/23
  6. */
  7. namespace service;
  8. use app\admin\model\wechat\WechatMessage;
  9. use behavior\wechat\MessageBehavior;
  10. use behavior\wechat\PaymentBehavior;
  11. use EasyWeChat\Foundation\Application;
  12. use EasyWeChat\Message\Article;
  13. use EasyWeChat\Message\Image;
  14. use EasyWeChat\Message\Material;
  15. use EasyWeChat\Message\News;
  16. use EasyWeChat\Message\Text;
  17. use EasyWeChat\Message\Video;
  18. use EasyWeChat\Message\Voice;
  19. use EasyWeChat\Payment\Order;
  20. use EasyWeChat\Server\Guard;
  21. use EasyWeChat\Support\XML;
  22. use think\Url;
  23. class WechatService
  24. {
  25. private static $instance = null;
  26. public static function options()
  27. {
  28. $wechat = SystemConfigService::more(['wechat_appid','wechat_appsecret','wechat_token']);
  29. $payment = SystemConfigService::more(['pay_weixin_mchid','pay_weixin_client_cert','pay_weixin_client_key','pay_weixin_key','pay_weixin_open']);
  30. $config = [
  31. 'app_id'=>isset($wechat['wechat_appid']) ? trim($wechat['wechat_appid']):'',
  32. 'secret'=>isset($wechat['wechat_appsecret']) ? trim($wechat['wechat_appsecret']):'',
  33. 'token'=>isset($wechat['wechat_token']) ? trim($wechat['wechat_token']):'',
  34. 'guzzle' => [
  35. 'timeout' => 10.0, // 超时时间(秒)
  36. ],
  37. ];
  38. if(isset($payment['pay_weixin_open']) && $payment['pay_weixin_open'] == 1){
  39. $config['payment'] = [
  40. 'merchant_id'=>trim($payment['pay_weixin_mchid']),
  41. 'key'=>trim($payment['pay_weixin_key']),
  42. 'cert_path'=>realpath('.'.$payment['pay_weixin_client_cert']),
  43. 'key_path'=>realpath('.'.$payment['pay_weixin_client_key']),
  44. 'notify_url'=>SystemConfigService::get('site_url').Url::build('wap/Wechat/notify')
  45. ];
  46. }
  47. return $config;
  48. }
  49. public static function application($cache = false)
  50. {
  51. (self::$instance === null || $cache === true) && (self::$instance = new Application(self::options()));
  52. return self::$instance;
  53. }
  54. public static function serve()
  55. {
  56. $wechat = self::application(true);
  57. $server = $wechat->server;
  58. self::hook($server);
  59. $response = $server->serve();
  60. exit($response->getContent());
  61. }
  62. /**
  63. * 监听行为
  64. * @param Guard $server
  65. */
  66. private static function hook($server)
  67. {
  68. $server->setMessageHandler(function($message){
  69. $behavior = MessageBehavior::class;
  70. HookService::beforeListen('wechat_message',$message,null,true,$behavior);
  71. switch ($message->MsgType){
  72. case 'event':
  73. switch (strtolower($message->Event)){
  74. case 'subscribe':
  75. if(isset($message->EventKey)){
  76. $response = HookService::resultListen('wechat_event_scan_subscribe',$message,$message->EventKey,true,$behavior);
  77. }else{
  78. $response = HookService::resultListen('wechat_event_subscribe',$message,null,true,$behavior);
  79. }
  80. break;
  81. case 'unsubscribe':
  82. $response = HookService::resultListen('wechat_event_unsubscribe',$message,null,true,$behavior);
  83. break;
  84. case 'scan':
  85. $response = HookService::resultListen('wechat_event_scan',$message,$message->EventKey,true,$behavior);
  86. break;
  87. case 'location':
  88. $response = HookService::resultListen('wechat_event_location',$message,null,true,$behavior);
  89. break;
  90. case 'click':
  91. $response = HookService::resultListen('wechat_event_click',$message,null,true,$behavior);
  92. break;
  93. case 'view':
  94. $response = HookService::resultListen('wechat_event_view',$message,null,true,$behavior);
  95. break;
  96. }
  97. break;
  98. case 'text':
  99. $response = HookService::resultListen('wechat_message_text',$message,null,true,$behavior);
  100. break;
  101. case 'image':
  102. $response = HookService::resultListen('wechat_message_image',$message,null,true,$behavior);
  103. break;
  104. case 'voice':
  105. $response = HookService::resultListen('wechat_message_voice',$message,null,true,$behavior);
  106. break;
  107. case 'video':
  108. $response = HookService::resultListen('wechat_message_video',$message,null,true,$behavior);
  109. break;
  110. case 'location':
  111. $response = HookService::resultListen('wechat_message_location',$message,null,true,$behavior);
  112. break;
  113. case 'link':
  114. $response = HookService::resultListen('wechat_message_link',$message,null,true,$behavior);
  115. break;
  116. // ... 其它消息
  117. default:
  118. $response = HookService::resultListen('wechat_message_other',$message,null,true,$behavior);
  119. break;
  120. }
  121. return $response;
  122. });
  123. }
  124. /**
  125. * 多客服消息转发
  126. * @param string $account
  127. * @return \EasyWeChat\Message\Transfer
  128. */
  129. public static function transfer($account = '')
  130. {
  131. $transfer = new \EasyWeChat\Message\Transfer();
  132. return empty($account) ? $transfer : $transfer->to($account);
  133. }
  134. /**
  135. * 上传永久素材接口
  136. * @return \EasyWeChat\Material\Material
  137. */
  138. public static function materialService()
  139. {
  140. return self::application()->material;
  141. }
  142. /**
  143. * 上传临时素材接口
  144. * @return \EasyWeChat\Material\Temporary
  145. */
  146. public static function materialTemporaryService()
  147. {
  148. return self::application()->material_temporary;
  149. }
  150. /**
  151. * 用户接口
  152. * @return \EasyWeChat\User\User
  153. */
  154. public static function userService()
  155. {
  156. return self::application()->user;
  157. }
  158. /**
  159. * 客服消息接口
  160. * @param null $to
  161. * @param null $message
  162. */
  163. public static function staffService()
  164. {
  165. return self::application()->staff;
  166. }
  167. /**
  168. * 微信公众号菜单接口
  169. * @return \EasyWeChat\Menu\Menu
  170. */
  171. public static function menuService()
  172. {
  173. return self::application()->menu;
  174. }
  175. /**
  176. * 微信二维码生成接口
  177. * @return \EasyWeChat\QRCode\QRCode
  178. */
  179. public static function qrcodeService()
  180. {
  181. return self::application()->qrcode;
  182. }
  183. /**
  184. * 短链接生成接口
  185. * @return \EasyWeChat\Url\Url
  186. */
  187. public static function urlService()
  188. {
  189. return self::application()->url;
  190. }
  191. /**
  192. * 用户授权
  193. * @return \Overtrue\Socialite\Providers\WeChatProvider
  194. */
  195. public static function oauthService()
  196. {
  197. return self::application()->oauth;
  198. }
  199. /**
  200. * 模板消息接口
  201. * @return \EasyWeChat\Notice\Notice
  202. */
  203. public static function noticeService()
  204. {
  205. return self::application()->notice;
  206. }
  207. public static function sendTemplate($openid,$templateId,array $data,$url = null,$defaultColor = null)
  208. {
  209. $notice = self::noticeService()->to($openid)->template($templateId)->andData($data);
  210. if($url !== null) $notice->url($url);
  211. if($defaultColor !== null) $notice->defaultColor($defaultColor);
  212. return $notice->send();
  213. }
  214. /**
  215. * 支付
  216. * @return \EasyWeChat\Payment\Payment
  217. */
  218. public static function paymentService()
  219. {
  220. return self::application()->payment;
  221. }
  222. public static function downloadBill($day,$type = 'ALL')
  223. {
  224. // $payment = self::paymentService();
  225. // $merchant = $payment->getMerchant();
  226. // $params = [
  227. // 'appid' => $merchant->app_id,
  228. // 'bill_date'=>$day,
  229. // 'bill_type'=>strtoupper($type),
  230. // 'mch_id'=> $merchant->merchant_id,
  231. // 'nonce_str' => uniqid()
  232. // ];
  233. // $params['sign'] = \EasyWeChat\Payment\generate_sign($params, $merchant->key, 'md5');
  234. // $xml = XML::build($params);
  235. // dump(self::paymentService()->downloadBill($day)->getContents());
  236. // dump($payment->getHttp()->request('https://api.mch.weixin.qq.com/pay/downloadbill','POST',[
  237. // 'body' => $xml,
  238. // 'stream'=>true
  239. // ])->getBody()->getContents());
  240. }
  241. public static function userTagService()
  242. {
  243. return self::application()->user_tag;
  244. }
  245. public static function userGroupService()
  246. {
  247. return self::application()->user_group;
  248. }
  249. /**
  250. * 生成支付订单对象
  251. * @param $openid
  252. * @param $out_trade_no
  253. * @param $total_fee
  254. * @param $attach
  255. * @param $body
  256. * @param string $detail
  257. * @param string $trade_type
  258. * @param array $options
  259. * @return Order
  260. */
  261. protected static function paymentOrder($openid,$out_trade_no,$total_fee,$attach,$body,$detail='',$trade_type='JSAPI',$options = [])
  262. {
  263. $total_fee = bcmul($total_fee,100,0);
  264. $order = array_merge(compact('openid','out_trade_no','total_fee','attach','body','detail','trade_type'),$options);
  265. if($order['detail'] == '') unset($order['detail']);
  266. return new Order($order);
  267. }
  268. /**
  269. * 获得下单ID
  270. * @param $openid
  271. * @param $out_trade_no
  272. * @param $total_fee
  273. * @param $attach
  274. * @param $body
  275. * @param string $detail
  276. * @param string $trade_type
  277. * @param array $options
  278. * @return mixed
  279. */
  280. public static function paymentPrepare($openid, $out_trade_no, $total_fee, $attach, $body, $detail='', $trade_type='JSAPI', $options = [])
  281. {
  282. $order = self::paymentOrder($openid,$out_trade_no,$total_fee,$attach,$body,$detail,$trade_type,$options);
  283. $result = self::paymentService()->prepare($order);
  284. if ($result->return_code == 'SUCCESS' && $result->result_code == 'SUCCESS'){
  285. try{
  286. HookService::listen('wechat_payment_prepare',$order,$result->prepay_id,false,PaymentBehavior::class);
  287. }catch (\Exception $e){}
  288. return $result->prepay_id;
  289. }else{
  290. if($result->return_code == 'FAIL'){
  291. exception('微信支付错误返回:'.$result->return_msg);
  292. }else if(isset($result->err_code)){
  293. exception('微信支付错误返回:'.$result->err_code_des);
  294. }else{
  295. exception('没有获取微信支付的预支付ID,请重新发起支付!');
  296. }
  297. exit;
  298. }
  299. }
  300. /**
  301. * 获得jsSdk支付参数
  302. * @param $openid
  303. * @param $out_trade_no
  304. * @param $total_fee
  305. * @param $attach
  306. * @param $body
  307. * @param string $detail
  308. * @param string $trade_type
  309. * @param array $options
  310. * @return array|string
  311. */
  312. public static function jsPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail='', $trade_type='JSAPI', $options = [])
  313. {
  314. return self::paymentService()->configForJSSDKPayment(self::paymentPrepare($openid,$out_trade_no,$total_fee,$attach,$body,$detail,$trade_type,$options));
  315. }
  316. /**
  317. * 使用商户订单号退款
  318. * @param $orderNo
  319. * @param $refundNo
  320. * @param $totalFee
  321. * @param null $refundFee
  322. * @param null $opUserId
  323. * @param string $refundReason
  324. * @param string $type
  325. * @param string $refundAccount
  326. */
  327. public static function refund($orderNo, $refundNo, $totalFee, $refundFee = null, $opUserId = null, $refundReason = '' , $type = 'out_trade_no', $refundAccount = 'REFUND_SOURCE_UNSETTLED_FUNDS')
  328. {
  329. $totalFee = floatval($totalFee);
  330. $refundFee = floatval($refundFee);
  331. return self::paymentService()->refund($orderNo,$refundNo,$totalFee,$refundFee,$opUserId,$type,$refundAccount,$refundReason);
  332. }
  333. public static function payOrderRefund($orderNo, array $opt)
  334. {
  335. if(!isset($opt['pay_price'])) exception('缺少pay_price');
  336. $totalFee = floatval(bcmul($opt['pay_price'],100,0));
  337. $refundFee = isset($opt['refund_price']) ? floatval(bcmul($opt['refund_price'],100,0)) : null;
  338. $refundReason = isset($opt['desc']) ? $opt['desc'] : '';
  339. $refundNo = isset($opt['refund_id']) ? $opt['refund_id'] : $orderNo;
  340. $opUserId = isset($opt['op_user_id']) ? $opt['op_user_id'] : null;
  341. $type = isset($opt['type']) ? $opt['type'] : 'out_trade_no';
  342. /*仅针对老资金流商户使用
  343. REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款)
  344. REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款*/
  345. $refundAccount = isset($opt['refund_account']) ? $opt['refund_account'] : 'REFUND_SOURCE_UNSETTLED_FUNDS';
  346. try{
  347. $res = (self::refund($orderNo,$refundNo,$totalFee,$refundFee,$opUserId,$refundReason,$type,$refundAccount));
  348. if($res->return_code == 'FAIL') exception('退款失败:'.$res->return_msg);
  349. if(isset($res->err_code)) exception('退款失败:'.$res->err_code_des);
  350. }catch (\Exception $e){
  351. exception($e->getMessage());
  352. }
  353. return true;
  354. }
  355. /**
  356. * 微信支付成功回调接口
  357. */
  358. public static function handleNotify()
  359. {
  360. self::paymentService()->handleNotify(function($notify, $successful){
  361. if($successful && isset($notify->out_trade_no)){
  362. WechatMessage::setOnceMessage($notify,$notify->openid,'payment_success',$notify->out_trade_no);
  363. return HookService::listen('wechat_pay_success',$notify,null,true,PaymentBehavior::class);
  364. }
  365. });
  366. }
  367. /**
  368. * jsSdk
  369. * @return \EasyWeChat\Js\Js
  370. */
  371. public static function jsService()
  372. {
  373. return self::application()->js;
  374. }
  375. public static function jsSdk($url = '')
  376. {
  377. $apiList = ['onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareWeibo', 'onMenuShareQZone', 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'pauseVoice', 'stopVoice', 'onVoicePlayEnd', 'uploadVoice', 'downloadVoice', 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'translateVoice', 'getNetworkType', 'openLocation', 'getLocation', 'hideOptionMenu', 'showOptionMenu', 'hideMenuItems', 'showMenuItems', 'hideAllNonBaseMenuItem', 'showAllNonBaseMenuItem', 'closeWindow', 'scanQRCode', 'chooseWXPay', 'openProductSpecificView', 'addCard', 'chooseCard', 'openCard'];
  378. $jsService = self::jsService();
  379. if($url) $jsService->setUrl($url);
  380. try{
  381. return $jsService->config($apiList);
  382. }catch (\Exception $e){
  383. return '{}';
  384. }
  385. }
  386. /**
  387. * 回复文本消息
  388. * @param string $content 文本内容
  389. * @return Text
  390. */
  391. public static function textMessage($content)
  392. {
  393. return new Text(compact('content'));
  394. }
  395. /**
  396. * 回复图片消息
  397. * @param string $media_id 媒体资源 ID
  398. * @return Image
  399. */
  400. public static function imageMessage($media_id)
  401. {
  402. return new Image(compact('media_id'));
  403. }
  404. /**
  405. * 回复视频消息
  406. * @param string $media_id 媒体资源 ID
  407. * @param string $title 标题
  408. * @param string $description 描述
  409. * @param null $thumb_media_id 封面资源 ID
  410. * @return Video
  411. */
  412. public static function videoMessage($media_id, $title = '', $description = '...', $thumb_media_id = null)
  413. {
  414. return new Video(compact('media_id','title','description','thumb_media_id'));
  415. }
  416. /**
  417. * 回复声音消息
  418. * @param string $media_id 媒体资源 ID
  419. * @return Voice
  420. */
  421. public static function voiceMessage($media_id)
  422. {
  423. return new Voice(compact('media_id'));
  424. }
  425. /**
  426. * 回复图文消息
  427. * @param string|array $title 标题
  428. * @param string $description 描述
  429. * @param string $url URL
  430. * @param string $image 图片链接
  431. */
  432. public static function newsMessage($title, $description = '...', $url = '', $image = '')
  433. {
  434. if(is_array($title)){
  435. if(isset($title[0]) && is_array($title[0])){
  436. $newsList = [];
  437. foreach ($title as $news){
  438. $newsList[] = self::newsMessage($news);
  439. }
  440. return $newsList;
  441. }else{
  442. $data = $title;
  443. }
  444. }else{
  445. $data = compact('title','description','url','image');
  446. }
  447. return new News($data);
  448. }
  449. /**
  450. * 回复文章消息
  451. * @param string|array $title 标题
  452. * @param string $thumb_media_id 图文消息的封面图片素材id(必须是永久 media_ID)
  453. * @param string $source_url 图文消息的原文地址,即点击“阅读原文”后的URL
  454. * @param string $content 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS
  455. * @param string $author 作者
  456. * @param string $digest 图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空
  457. * @param int $show_cover_pic 是否显示封面,0为false,即不显示,1为true,即显示
  458. * @param int $need_open_comment 是否打开评论,0不打开,1打开
  459. * @param int $only_fans_can_comment 是否粉丝才可评论,0所有人可评论,1粉丝才可评论
  460. * @return Article
  461. */
  462. public static function articleMessage($title, $thumb_media_id, $source_url, $content = '', $author = '', $digest = '', $show_cover_pic = 0, $need_open_comment = 0, $only_fans_can_comment = 1)
  463. {
  464. $data = is_array($title) ? $title : compact('title','thumb_media_id','source_url','content','author','digest','show_cover_pic','need_open_comment','only_fans_can_comment');
  465. return new Article($data);
  466. }
  467. /**
  468. * 回复素材消息
  469. * @param string $type [mpnews、 mpvideo、voice、image]
  470. * @param string $media_id 素材 ID
  471. * @return Material
  472. */
  473. public static function materialMessage($type, $media_id)
  474. {
  475. return new Material($type,$media_id);
  476. }
  477. /**
  478. * 作为客服消息发送
  479. * @param $to
  480. * @param $message
  481. * @return bool
  482. */
  483. public static function staffTo($to, $message)
  484. {
  485. $staff = self::staffService();
  486. $staff = is_callable($message) ? $staff->message($message()) : $staff->message($message);
  487. $res = $staff->to($to)->send();
  488. HookService::afterListen('wechat_staff_to',compact('to','message'),$res);
  489. return $res;
  490. }
  491. /**
  492. * 获得用户信息
  493. * @param array|string $openid
  494. * @return \EasyWeChat\Support\Collection
  495. */
  496. public static function getUserInfo($openid)
  497. {
  498. $userService = self::userService();
  499. $userInfo = is_array($openid) ? $userService->batchGet($openid) : $userService->get($openid);
  500. return $userInfo;
  501. }
  502. }