Push.php 14 KB

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