微信公众号开发 – 自定义菜单 – 消息推送

现在很多网站或者APP产品推广或者业务需求都离不开微信的大量用户平台的接入了,所以微信公众号开发是志在必得的,需要开发起来才能获取微信带来的用户便利推广和及时的通知用户相关的信息,以下来说明一下自定义菜单和消息推送的开发流程和相关要点。

一、微信开启自定义菜单时需要在微信官网开启服务器开发模式,然后就不能在官网上设置菜单了,只能通过自定义开发接口设置菜单了。第一步要进行微信校验自己的服务器:在微信官网设置服务器校验token,然后在自己服务器上代码加入校验参数进行认证,参数有nonce,timestamp,signature,echostr(第一次接入参数才有)

// 形成数组,然后按字典序排序
$array = [$nonce, $timestamp, $token];
sort($array);
// 拼接成字符串,sha1加密 ,然后与signature进行校验
$str = sha1(implode($array));
if ($str == $signature && $echostr) {
      // 第一次接入weixin api接口的时候
      return new Response($echostr);
}

二、第一步服务器校验完成后进行用户的消息接收和消息推送、自定义菜单的开发:

1.接收用户消息(用户必须官网服务号或者订阅号才可以):

// 1.获取到微信推送过来post数据(xml格式)
$postArr = isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : file_get_contents("php://input");
// 2.处理消息类型,并设置回复类型和内容
$postObj = simplexml_load_string($postArr);
// 取得用户的微信openid
$toUser = $postObj->FromUserName;
// 取得用户微信信息
$userInfo = $this->getUserInfo($toUser);
// 保存用户信息
// TODO自己服务器保存微信用户信息
// 设置微信公众号自定义菜单
$this->definedMenuItem();
// 返回指定的消息
$returnContent = $this->reponseMsg($postObj);
return new Response($returnContent);

2.处理用户消息:

/*
 * 消息处理函数
 */
private function reponseMsg($object) {
  $toUser = $object->FromUserName;
  $fromUser = $object->ToUserName;
  $returnStr = '';
  // 判断该数据包是否是订阅、菜单点击的事件推送
  if (strtolower($object->MsgType) == 'event') {
    // 如果是关注 subscribe 事件
    switch (strtolower($object->Event)) {
      case 'subscribe':
        // 回复用户消息(纯文本格式)
        $content = '欢迎进入***服务号!';
        $returnStr = $this->sendTextMessage($toUser, $fromUser, $content);
        break;
      case 'click':
        $eventKey = $object->EventKey; // 获取key
        // 取得相应Key的文章内容
        $wxpublicMenuRepository = $this->getRepository('WxpublicMenu');
        $wxpublicMenu = $wxpublicMenuRepository->findOneBy(['key' => $eventKey]);
        $content = $wxpublicMenu->getContent();
        if ($wxpublicMenu && !empty($content)) {
          $returnStr = $this->sendTextMessage($toUser, $fromUser, $content);
        } else {
          $wxpublicMenuArticleRepository = $this->getRepository('WxpublicMenuArticle');
          $wxpublicMenuArticles = $wxpublicMenuArticleRepository->getWxpublicMenuArticleList(['key' => $eventKey]);
          $returnStr = $this->sendImgTextMessage($toUser, $fromUser, $wxpublicMenuArticles);
        }
        break;
    }
  }
  // 根据用户输入消息进行回复
  elseif (strtolower($object->MsgType) == 'text') {
    // 用户输入的文本消息
    $keyword = trim($object->Content);
    switch ($keyword) {
      case 'location':
      case '位置':
        $content = '点击链接查询您当前的位置: http://map.baidu.com/';
        $returnStr = $this->sendTextMessage($toUser, $fromUser, $content);
        break;
    }
  }
  return $returnStr;
}

3.微信公众号处理基类,发送(文本、图文、推送)消息的方法都在基类里面:

/* 
 * Description of AbstractLogicTrait
 * Business Logic Controller
 */
trait AbstractWxmsgTrait
{
    // 微信公众号所需的AccessToken的缓存文件名
    private $wxAccessTokenCacheFileName = '_wx_access_token_cache_file/access_token-date-%s.txt';

    //
    // 微信公众平台取得ACCESS TOKEN,并做两个小时的缓存处理,缓存说明请看官网
    // param boolean $isRefresh 是否刷新AccessToken
    // param sring $url 访问页面的URL
    // return string
    //
    protected function getWxAccessToken ($isRefresh = false)
    {
        $accessToken = '';
        try {
            // 取得文件缓存服务
            $cacheFileHandler = $this->get('cache.file.handler');
            // 取得缓存的微信公众号的AccessToken
            $nowDatetime = DateHelper::getNowDateTime();
            $wxAccessTokenCacheFileName = sprintf($this->wxAccessTokenCacheFileName, $nowDatetime->format('Ymd'));
            $cacheArray = $cacheFileHandler->readFileByName($wxAccessTokenCacheFileName);
            if (!$isRefresh && !empty($cacheArray) && isset($cacheArray['contents']) && !empty($cacheArray['contents'])
                && isset($cacheArray['contents']['access_token']) && !empty($cacheArray['contents']['expires_in'])) {
                $accessToken = $cacheArray['contents']['access_token'];
            } else {
                // 取得CURL服务
                $curl = $this->get('curl.handler');
                $getAccessTokenUrl = $this->getCfgParameter('wx_public_get_access_token_url');
                $result = $curl->curlRequest($getAccessTokenUrl, [], [], false);
                if (! empty($result)) {
                    $resultArray = json_decode($result, true);
                    if (isset($resultArray['access_token']) && isset($resultArray['expires_in'])) {
                        $accessToken = $resultArray['access_token'];
                        //缓存微信公众号所需的AccessToken
                        $cacheFileHandler->cacheFileByName($wxAccessTokenCacheFileName, $resultArray, $resultArray['expires_in']);
                    }
                }
            }
        } catch (\Exception $e) {
            $this->generateApiLog(__FUNCTION__ . $e->getMessage());
        }
        return $accessToken;
    }

    // TODO推送文本和图文消息含有XML标准的标签不能发布文章,所以截图放到后面。

    //
    // 发送模板消息
    // param type $toUser 
    // param type $templateId
    // param type $url
    // param type $data
    // return type
    //
    protected function sendTemplateMessage ($toUser, $templateId, $url, $data)
    {
        $message = ['touser' =>$toUser
            , 'template_id' => $templateId
            , 'url' => $url, 'data' => $data];
        $messageJson = urldecode(json_encode($message));
        $accessToken = $this->getWxAccessToken();
        $postUrl = $this->getCfgParameter('wx_public_send_template_message');
        $postUrl = str_replace('access_token_str' , $accessToken, $postUrl);
        return $this->post($postUrl, $messageJson);
    }

    //
    // 获取用户基本信息
    // openid:用户的openid。用户与公众号唯一身份标识
    // 若用户未关注公众号,无法获取详细信息
    //
    protected function getUserInfo($openid)
    {
        $apiErrorLogPath = $this->getCfgParameter('api_error_log_path') . 'WXMSG_PUSH_OPENID/';
        StringHelper::mkdirs($apiErrorLogPath);

        $accessToken = $this->getWxAccessToken();
        $url = $this->getCfgParameter('wx_public_cgi_get_userinfo_url');
        $url = str_replace(['access_token_str','openid_str'] , [$accessToken, $openid], $url);
        $content = file_get_contents($url);
        $this->generateCustomedLog($apiErrorLogPath, '用户openid:' . $openid . ', 获取用户信息:' .$content, 0);
        $userInfo = json_decode($content, true);
        // 如果请求微信用户信息失败则更新AccessToken的值并重新获取
        if (isset($userInfo['errcode']) && !empty($userInfo['errcode'])) {
            $this->getWxAccessToken(true);
            $userInfo = $this->getUserInfo($openid);
        }
        return $userInfo;
    }

    //
    // 微信公众号自定义菜单
    //
    protected function definedMenuItem ()
    {
        $button = [];
        $accessToken = $this->getWxAccessToken();
        $url = $this->getCfgParameter('wx_public_menu_create');
        $url = str_replace('access_token_str' , $accessToken, $url);
        // 取得所有微信菜单
        $wxpublicMenuRepository = $this->getRepository('WxpublicMenu');
        $menus = $wxpublicMenuRepository->findBy(['parentId' => 0], ['parentId' => 'ASC', 'sort' => 'ASC']);
        foreach ($menus as $menu) {
            $submenus = $wxpublicMenuRepository->findBy(['parentId' => $menu->getId()], ['sort' => 'ASC']);
            // 父级菜单
            if (count($submenus) == 0) {
                $menuArray = $this->getMenuArray($menu);
                array_push($button, $menuArray);
            } else {
                $subButtons = [];
                foreach ($submenus as $submenu) {
                    $menuArray = $this->getMenuArray($submenu);
                    array_push($subButtons, $menuArray);
                }
                array_push($button, ["name"=>$menu->getName(),"sub_button"=>$subButtons]);
            }
        }
        $postArr = ["button"=>$button];
        $postJson = urlencode(json_encode($postArr));
        $this->post($url, $postJson);
    }

    //
    // 处理单个菜单类型及值的设置
    // param array $menu
    // return array
    //
    protected function getMenuArray ($menu)
    {
        $menuArray = [];
        switch ($menu->getType()) {
            case 'click':
                $menuArray = ["name"=>$menu->getName(),"type"=>$menu->getType(),"key"=>$menu->getKey()];
                break;
            case 'view':
                $menuArray = ["name"=>$menu->getName(),"type"=>$menu->getType(),"url"=>$menu->getUrl()];
                break;
        }
        return $menuArray;
    }

    //
    // post 指定URL请求
    // param string $url
    // param string $data
    // return string
    //
    protected function post ($url, $data)
    {
        $opts = [
            'http' => [
                'method' => 'POST',
                'header' => 'Content-type: application/x-www-form-urlencoded',
                'content' => $data
            ]
        ];

        $context = stream_context_create($opts);
        $result = file_get_contents($url, false, $context);
        return $result;
    }
}

※推送文本和图文消息含有XML标准的标签不能发布文章,所以截图放到这里:

推送文本和图文消息含有XML标准的标签代码截图

4.微信公众号开发相关的请求接口:

#微信公众号授权URL
https://open.weixin.qq.com/connect/oauth2/authorize?appid=%wx_public_appid%&redirect_uri=redirect_uri_str&response_type=code&scope=snsapi_userinfo&state=state_code#wechat_redirect

#微信公众号获取微信Web页面授权AccessToken的URL
https://api.weixin.qq.com/sns/oauth2/access_token?appid=%wx_public_appid%&secret=%wx_public_app_secret%&code=code_str&grant_type=authorization_code

#微信公众号获取微信Web页面授权AccessToken刷新AccessToken的URL
https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%wx_public_appid%&grant_type=refresh_token&refresh_token=refresh_token_str

#微信公众号使用AccessToken取得用户信息,使用于授权时取得用户信息
https://api.weixin.qq.com/sns/userinfo?access_token=access_token_str&openid=openid_str&lang=zh_CN

#微信公众平台开发取得ACCESS TOKEN,使用于网页分享和取得用户信息
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%wx_public_appid%&secret=%wx_public_app_secret%

#微信公众平台网页JS开发取得jsapi_ticket,是公众号用于调用微信JS接口的临时票据
https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=access_token_str&type=jsapi

#微信公众号使用AccessToken取得用户信息,使用于发送微信通知时取得用户信息
https://api.weixin.qq.com/cgi-bin/user/info?access_token=access_token_str&openid=openid_str&lang=zh_CN

#发送模板消息给用户
https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=access_token_str

#微信公众号自定义菜单
https://api.weixin.qq.com/cgi-bin/menu/create?access_token=access_token_str