Push.php 14 KB

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