Push.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. <?php
  2. namespace app\wechat\controller\api;
  3. use app\wechat\service\FansService;
  4. use app\wechat\service\MediaService;
  5. use app\wechat\service\WechatService;
  6. use think\admin\Controller;
  7. /**
  8. * 微信消息推送处理
  9. * Class Push
  10. * @package app\wechat\controller\api
  11. */
  12. class Push extends Controller
  13. {
  14. /**
  15. * 公众号 APPID
  16. * @var string
  17. */
  18. protected $appid;
  19. /**
  20. * 微信用户 OPENID
  21. * @var string
  22. */
  23. protected $openid;
  24. /**
  25. * 消息是否加密码
  26. * @var boolean
  27. */
  28. protected $encrypt;
  29. /**
  30. * 请求微信 OPENID
  31. * @var string
  32. */
  33. protected $fromOpenid;
  34. /**
  35. * 微信消息对象
  36. * @var array
  37. */
  38. protected $receive;
  39. /**
  40. * 微信实例对象
  41. * @var \WeChat\Receive
  42. */
  43. protected $wechat;
  44. /**
  45. * 强制返回JSON消息
  46. * @var boolean
  47. */
  48. protected $forceJson = false;
  49. /**
  50. * 强制客服消息回复
  51. * @var boolean
  52. */
  53. protected $forceCustom = false;
  54. /**
  55. * 获取网络出口IP
  56. * @return mixed
  57. */
  58. public function geoip()
  59. {
  60. return $this->request->ip();
  61. }
  62. /**
  63. * 消息推送处理接口
  64. * @return string
  65. */
  66. public function index()
  67. {
  68. try {
  69. if (WechatService::instance()->getType() === 'thr') {
  70. $this->forceJson = true; // 直接返回JSON数据到SERVICE
  71. $this->forceCustom = false; // 直接使用客服消息模式推送
  72. $this->appid = $this->request->post('appid', '', null);
  73. $this->openid = $this->request->post('openid', '', null);
  74. $this->encrypt = boolval($this->request->post('encrypt', 0));
  75. $this->receive = $this->_arrayChangeKeyCase(json_decode(input('params', '[]'), true));
  76. if (empty($this->appid) || empty($this->openid) || empty($this->receive)) {
  77. throw new \think\admin\Exception('微信API实例缺失必要参数[appid,openid,receive]');
  78. }
  79. } else {
  80. $this->forceJson = false; // 直接返回JSON对象数据
  81. $this->forceCustom = false; // 直接使用客服消息推送
  82. $this->appid = WechatService::instance()->getAppid();
  83. $this->wechat = WechatService::WeChatReceive();
  84. $this->openid = $this->wechat->getOpenid();
  85. $this->encrypt = $this->wechat->isEncrypt();
  86. $this->receive = $this->_arrayChangeKeyCase($this->wechat->getReceive());
  87. }
  88. $this->fromOpenid = $this->receive['tousername'];
  89. // 消息类型:text, event, image, voice, shortvideo, location, link
  90. if (method_exists($this, ($method = $this->receive['msgtype']))) {
  91. if (is_string($result = $this->$method())) return $result;
  92. } else {
  93. $this->app->log->notice("The {$method} event pushed by wechat was not handled. from {$this->openid}");
  94. }
  95. } catch (\Exception $exception) {
  96. $this->app->log->error("{$exception->getFile()}:{$exception->getLine()} [{$exception->getCode()}] {$exception->getMessage()}");
  97. }
  98. return 'success';
  99. }
  100. /**
  101. * 文件消息处理
  102. * @return boolean|string
  103. * @throws \WeChat\Exceptions\InvalidDecryptException
  104. * @throws \WeChat\Exceptions\InvalidResponseException
  105. * @throws \WeChat\Exceptions\LocalCacheException
  106. * @throws \think\admin\Exception
  107. * @throws \think\db\exception\DataNotFoundException
  108. * @throws \think\db\exception\DbException
  109. * @throws \think\db\exception\ModelNotFoundException
  110. */
  111. protected function text()
  112. {
  113. return $this->_keys("WechatKeys#keys#{$this->receive['content']}", false, $this->forceCustom);
  114. }
  115. /**
  116. * 事件消息处理
  117. * @return boolean|string
  118. * @throws \WeChat\Exceptions\InvalidDecryptException
  119. * @throws \WeChat\Exceptions\InvalidResponseException
  120. * @throws \WeChat\Exceptions\LocalCacheException
  121. * @throws \think\admin\Exception
  122. * @throws \think\db\exception\DataNotFoundException
  123. * @throws \think\db\exception\DbException
  124. * @throws \think\db\exception\ModelNotFoundException
  125. */
  126. protected function event()
  127. {
  128. switch (strtolower($this->receive['event'])) {
  129. case 'unsubscribe':
  130. $this->app->event->trigger('WechatFansUnSubscribe', $this->openid);
  131. return $this->_setUserInfo(false);
  132. case 'subscribe':
  133. [$this->app->event->trigger('WechatFansSubscribe', $this->openid), $this->_setUserInfo(true)];
  134. if (isset($this->receive['eventkey']) && is_string($this->receive['eventkey'])) {
  135. if (($key = preg_replace('/^qrscene_/i', '', $this->receive['eventkey']))) {
  136. return $this->_keys("WechatKeys#keys#{$key}", false, true);
  137. }
  138. }
  139. return $this->_keys('WechatKeys#keys#subscribe', true, $this->forceCustom);
  140. case 'scan':
  141. case 'click':
  142. if (empty($this->receive['eventkey'])) return false;
  143. return $this->_keys("WechatKeys#keys#{$this->receive['eventkey']}", false, $this->forceCustom);
  144. case 'scancode_push':
  145. case 'scancode_waitmsg':
  146. if (empty($this->receive['scancodeinfo']['scanresult'])) return false;
  147. return $this->_keys("WechatKeys#keys#{$this->receive['scancodeinfo']['scanresult']}", false, $this->forceCustom);
  148. case 'view':
  149. case 'location':
  150. default:
  151. return false;
  152. }
  153. }
  154. /**
  155. * 关键字处理
  156. * @param string $rule 关键字规则
  157. * @param boolean $last 重复回复消息处理
  158. * @param boolean $custom 是否使用客服消息发送
  159. * @return boolean|string
  160. * @throws \WeChat\Exceptions\InvalidDecryptException
  161. * @throws \WeChat\Exceptions\InvalidResponseException
  162. * @throws \WeChat\Exceptions\LocalCacheException
  163. * @throws \think\admin\Exception
  164. * @throws \think\db\exception\DataNotFoundException
  165. * @throws \think\db\exception\DbException
  166. * @throws \think\db\exception\ModelNotFoundException
  167. */
  168. private function _keys(string $rule, bool $last = false, bool $custom = false)
  169. {
  170. if (is_numeric(stripos($rule, '#reply#text:'))) {
  171. [, $content] = explode('#reply#text:', $rule);
  172. return $this->_buildMessage('text', ['Content' => $content]);
  173. }
  174. [$table, $field, $value] = explode('#', $rule . '##');
  175. $data = $this->app->db->name($table)->where([$field => $value])->find();
  176. if (empty($data['type']) || (array_key_exists('status', $data) && empty($data['status']))) {
  177. return $last ? false : $this->_keys('WechatKeys#keys#default', true, $custom);
  178. }
  179. switch (strtolower($data['type'])) {
  180. case 'keys':
  181. $content = empty($data['content']) ? $data['name'] : $data['content'];
  182. return $this->_keys("WechatKeys#keys#{$content}", $last, $custom);
  183. case 'text':
  184. return $this->_sendMessage('text', ['content' => $data['content']], $custom);
  185. case 'customservice':
  186. return $this->_sendMessage('customservice', ['content' => $data['content']], false);
  187. case 'voice':
  188. if (empty($data['voice_url']) || !($mediaId = MediaService::instance()->upload($data['voice_url'], 'voice'))) return false;
  189. return $this->_sendMessage('voice', ['media_id' => $mediaId], $custom);
  190. case 'image':
  191. if (empty($data['image_url']) || !($mediaId = MediaService::instance()->upload($data['image_url'], 'image'))) return false;
  192. return $this->_sendMessage('image', ['media_id' => $mediaId], $custom);
  193. case 'news':
  194. [$news, $articles] = [MediaService::instance()->news($data['news_id']), []];
  195. if (empty($news['articles'])) return false;
  196. foreach ($news['articles'] as $vo) array_push($articles, [
  197. 'url' => url("@wechat/api.view/item/id/{$vo['id']}", [], false, true)->build(),
  198. 'title' => $vo['title'], 'picurl' => $vo['local_url'], 'description' => $vo['digest'],
  199. ]);
  200. return $this->_sendMessage('news', ['articles' => $articles], $custom);
  201. case 'music':
  202. if (empty($data['music_url']) || empty($data['music_title']) || empty($data['music_desc'])) return false;
  203. $mediaId = $data['music_image'] ? MediaService::instance()->upload($data['music_image'], 'image') : '';
  204. return $this->_sendMessage('music', [
  205. 'hqmusicurl' => $data['music_url'], 'musicurl' => $data['music_url'],
  206. 'description' => $data['music_desc'], 'title' => $data['music_title'], 'thumb_media_id' => $mediaId,
  207. ], $custom);
  208. case 'video':
  209. if (empty($data['video_url']) || empty($data['video_desc']) || empty($data['video_title'])) return false;
  210. $video = ['title' => $data['video_title'], 'introduction' => $data['video_desc']];
  211. if (!($mediaId = MediaService::instance()->upload($data['video_url'], 'video', $video))) return false;
  212. return $this->_sendMessage('video', ['media_id' => $mediaId, 'title' => $data['video_title'], 'description' => $data['video_desc']], $custom);
  213. default:
  214. return false;
  215. }
  216. }
  217. /**
  218. * 发送消息到微信
  219. * @param string $type 消息类型(text|image|voice|video|music|news|mpnews|wxcard)
  220. * @param array $data 消息内容数据对象
  221. * @param boolean $custom 是否使用客服消息发送
  222. * @return string|void
  223. * @throws \WeChat\Exceptions\InvalidDecryptException
  224. * @throws \WeChat\Exceptions\InvalidResponseException
  225. * @throws \WeChat\Exceptions\LocalCacheException
  226. */
  227. private function _sendMessage(string $type, array $data, bool $custom = false)
  228. {
  229. if ($custom) {
  230. WechatService::WeChatCustom()->send(['touser' => $this->openid, 'msgtype' => $type, $type => $data]);
  231. } else switch (strtolower($type)) {
  232. case 'text': // 发送文本消息
  233. return $this->_buildMessage($type, ['Content' => $data['content']]);
  234. case 'news': // 发送图文消息
  235. foreach ($data['articles'] as &$v) {
  236. $v = ['PicUrl' => $v['picurl'], 'Title' => $v['title'], 'Description' => $v['description'], 'Url' => $v['url']];
  237. }
  238. return $this->_buildMessage($type, ['Articles' => $data['articles'], 'ArticleCount' => count($data['articles'])]);
  239. case 'image': // 发送图片消息
  240. return $this->_buildMessage($type, ['Image' => ['MediaId' => $data['media_id']]]);
  241. case 'voice': // 发送语言消息
  242. return $this->_buildMessage($type, ['Voice' => ['MediaId' => $data['media_id']]]);
  243. case 'video': // 发送视频消息
  244. return $this->_buildMessage($type, ['Video' => ['Title' => $data['title'], 'Description' => $data['description'], 'MediaId' => $data['media_id']]]);
  245. case 'music': // 发送音乐消息
  246. return $this->_buildMessage($type, ['Music' => ['Title' => $data['title'], 'Description' => $data['description'], 'MusicUrl' => $data['musicurl'], 'HQMusicUrl' => $data['musicurl'], 'ThumbMediaId' => $data['thumb_media_id']]]);
  247. case 'customservice': // 转交客服消息
  248. if ($data['content']) $this->_sendMessage('text', $data, true);
  249. return $this->_buildMessage('transfer_customer_service', []);
  250. default:
  251. return 'success';
  252. }
  253. }
  254. /**
  255. * 消息数据生成
  256. * @param mixed $type 消息类型
  257. * @param array $data 消息内容
  258. * @return string
  259. * @throws \WeChat\Exceptions\InvalidDecryptException
  260. */
  261. private function _buildMessage(string $type, array $data = []): string
  262. {
  263. $data = array_merge($data, ['ToUserName' => $this->openid, 'FromUserName' => $this->fromOpenid, 'CreateTime' => time(), 'MsgType' => $type]);
  264. return $this->forceJson ? json_encode($data, JSON_UNESCAPED_UNICODE) : WechatService::WeChatReceive()->reply($data, true, $this->encrypt);
  265. }
  266. /**
  267. * 同步粉丝状态
  268. * @param boolean $state 订阅状态
  269. * @return boolean
  270. * @throws \think\db\exception\DataNotFoundException
  271. * @throws \think\db\exception\DbException
  272. * @throws \think\db\exception\ModelNotFoundException
  273. */
  274. private function _setUserInfo(bool $state): bool
  275. {
  276. if ($state) {
  277. try {
  278. $user = WechatService::WeChatUser()->getUserInfo($this->openid);
  279. return FansService::instance()->set(array_merge($user, ['subscribe' => 1, 'appid' => $this->appid]));
  280. } catch (\Exception $exception) {
  281. $this->app->log->error(__METHOD__ . " {$this->openid} get userinfo faild. {$exception->getMessage()}");
  282. return false;
  283. }
  284. } else {
  285. return FansService::instance()->set(['subscribe' => 0, 'openid' => $this->openid, 'appid' => $this->appid]);
  286. }
  287. }
  288. /**
  289. * 数组健值全部转小写
  290. * @param array $data
  291. * @return array
  292. */
  293. private function _arrayChangeKeyCase(array $data): array
  294. {
  295. $data = array_change_key_case($data, CASE_LOWER);
  296. foreach ($data as $key => $vo) if (is_array($vo)) {
  297. $data[$key] = $this->_arrayChangeKeyCase($vo);
  298. }
  299. return $data;
  300. }
  301. }