Push.php 14 KB

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