Ela's Notes
So Be It


Hitokoto ·
[PHP]用php实现微信订阅号自定义菜单和自动回复
Elatis   PHP   1072 | 文章字数: 14581 字

本篇将介绍如何使用php来实现微信订阅号的一些功能

本篇会涉及到一些php的基本语法,在这篇文章中我会穿插一些粗略的讲解,如果在看了我的讲解后想更清楚地了解php相关知识的可以看这里:PHP教程

申请接口测试号,并通过服务器验证

申请接口测试号

可以点这里申请一个接口测试号
为什么不自己注册一个订阅号呢,因为主体为个人的订阅号无法调用大部分接口.
你会看到一个如下图所示的界面

这样你就完成了接口测试号的申请.

验证服务器资源

接着要通过服务器资源的验证.
我先来解释一下要填的URLToken都是什么.

  • URL:虽然他上面说的是URL,但其实要填的是你服务器上相应的cgi程序的地址,如我自己填的就是index.php这个cgi程序的地址
    我的上篇教程走下来的话应该已经能成功使用cgi程序了,我就不多多解释了.
  • Token:就相当于一个密码,它伴随着其它数据一起通过get表单被发送给你的服务器,让你验证确实是来自微信服务器的信息并且是你自己填的信息.
    Token还有一点要求就是独一无二,也就是你要想一个尽可能不和别人重复的Token,否则可能会莫名奇妙提交失败.因为接口测试号提交失败时会神奇的不显示错误类型,和别的错误混淆了就难调试了.

然后我们就开始想办法通过它的认证吧.
首先我们看看微信服务器会给我们的服务器发什么东西

其实你根本不用管这些东西都代表了什么,只要按他说的操作即可.
步骤大概就是这样的:

  • 接收get表单
  • 对token,nonce,timestamp排序,并合并成一个字符串
  • 对合并后的字符串进行sha1加密
  • 与signature进行比较,如果相同则返回echostr
  • 否则不返回

放心,接收表单,排序合并,sha1加密这些操作php都有自带的函数.

  • 关于获取GET表单
    php有自带的超全局变量$_GET专门用来接收发送给php脚本的GET表单的内容,所以我们直接调用这个超全局变量即可.
    $_GET中的数据都以键值对的方式进行存储.例如,假设前端发来的GET表单中有一个名称为test的元素,而要获得那个元素的内容,只要$_GET['test']即可.
    按上面的说法,我们只需要这样操作就可以接收GET表单啦
    <?php
    $token = "wdnb";//token不会被发送给你,手动填上你在接口测试号申请页面填的吧
    //获取GET表单中的内容
    $signature = $_GET["signature"];
    $timestamp = $_GET["timestamp"];
    $nonce = $_GET["nonce"];
    $echostr = $_GET["echostr"];
    ?>

接下来的操作就更简单了,解释我直接写在代码里吧

<?php
$token = "wdnb";                            //token不会被发送给你,手动填上你在接口测试号申请页面填的吧
                                            //获取GET表单中的内容
$signature = $_GET["signature"];
$timestamp = $_GET["timestamp"];
$nonce = $_GET["nonce"];
$echostr = $_GET["echostr"];

$tmpArr = array($token, $timestamp, $nonce);//形成一个数组
sort($tmpArr);                              //排序
$tmpStr = implode($tmpArr);                 //排序后合并成一个字符串
$tmpStr = sha1($tmpStr);                    //进行sha1加密
if ($tmpStr == $signature) {                //与signature比较
                                            //如果相同则返回echostr
    //如果你无法通过请加上这两行试试
    //header("content-type:text");
    //ob_clean();
    echo $echostr;
    return true;
} else {                                    //否则donothing
    echo "GG";
    return false;
}
?>

将文件保存为index.php,放在你设置的虚拟主机根目录的你想要的位置下
在申请网页上填上相应的路径,并点击提交
如果出现成功字样则说明你通过了验证,这个验证服务器资源的代码可以暂时删了,但是index.php还是要保留,因为以后微信服务器就通过那个cgi程序与你交互了

搭建订阅号

实现自定义菜单

实现自定义菜单用的是微信提供的API.下面将分三个部分进行介绍

编写自定义表单样式

首先来编写自定义菜单的样式.
以下代码保存为menu.json,放到虚拟主机根目录下你想要的位置:

{
    "button": [
        {
            "type": "click",
            "name": "一键牛逼",
            "key": "oneClickNB"
        },
        {
            "type": "click",
            "name": "作者博客",
            "key": "myBlog"
        },
        {
            "name": "获取图片",
            "sub_button": [
                {
                    "type": "click",
                    "name": "图片",
                    "key": "picture"
                },
                {
                    "type": "click",
                    "name": "还是图片",
                    "key": "alsoPicture"
                }
            ]
        }
    ]
}
  • 参数解释:
    button:表示中括号内填的都是按钮的设置,也就是微信订阅号界面底下那三个东西
    type:按钮的类别,触发该按钮后会随着post表单发送给你的服务器,元素名为Event
    name:订阅号界面上显示的按钮名字
    key:按钮的键值,触发该按钮后会随着post表单发送给你的服务器,元素名为EvnetKey

不以字符串变量形式写在即将编写的发送表单的代码内的原因就是写在单独的文件时,可以通过vscode等编辑器查看高亮,并且还有纠错功能,减少了出bug的几率.

编写获取access_token的代码

access_token和前面提到的token不一样.token是你验证资源是否来自微信服务器的凭证,access_token正相反,它是微信服务器验证资源是否来自你的服务器的凭证.
虽然access_token的有效时间为30分钟,但是一天能调用几千次之多,所以只需要让程序在需要调用时自动获取并调用即可.
根据微信公众号的接口调用说明,我们可以很轻松的写出以下代码(比C++作业还简单)
额外说明:php中连接字符串用的是.,而不是+
以下代码保存为get_access_token.php,放在虚拟主机根目录下你想要放的位置:

<?php
function getAccessToken()
{
    //已打码处理
    $appId = 'wxa5xxxxxxxxxxxxx';
    $appSecret = '692e8xxxxxxxxxxxxxxxxxx';

    //微信提供的接口地址,根据文档,表单中加入你的appid和app密钥,在接口申请界面最上方可以找到
    $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential'
        .'&appid=' . $appId . '&secret=' . $appSecret;
    //发送表单使用的是curl,以下是发送GET表单的示例

    //初始化一个curl对象
    $curl = curl_init();
    //设置调用接口后的返回为原生(RAW)的内容
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    //设置超时时间
    curl_setopt($curl, CURLOPT_TIMEOUT, 500);
    //设置表单发送地址
    curl_setopt($curl, CURLOPT_URL, $url);
    //执行发送,并获取返回值保存到一个对象内
    $accessToken = curl_exec($curl);
    //关闭这个curl对象
    curl_close($curl);
    //将得到的原生内容以json的形式进行解释,并获取其中的access_token元素所对应的值
    //也就是我们最终需要的access_token
    $res = json_decode($accessToken)->access_token;
    return $res;
}
?>

编写上传自定义表单的代码

自定义菜单功能和上面的验证资源不一样,是通过你主动向微信服务器发请求实现的.
所以我们不能再使用index.php,因为它是负责接收微信服务器发过来的信息的.
将以下代码保存为setmenu.php,放到你想要的虚拟主机根目录下的位置

<?php
#引用刚才写的获取access_token的代码,填的是相对这个代码文件的路径
include './src/php/get_access_token.php';
#获取access_token
$accessToken = getAccessToken();
#微信自定义菜单接口url,后面接上刚刚获取的access_token
$url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=".$accessToken;
#菜单配置文件名,同样是相对这个代码文件的路径
$fileName = "./src/logs/menu.json";

#读取menu.json保存到menuSettings变量
$menuFile = fopen($fileName, "r") or die("Unable to open file");
$menuSettings = fread($menuFile, filesize(($fileName)));
fclose($menuFile);

#发送post请求
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
//这一步是设置http报头
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Content-Length:' . strlen($menuSettings)));
curl_setopt($curl, CURLOPT_POSTFIELDS, $menuSettings);
//发送post表单
$res = curl_exec($curl);
curl_close($curl);

然后在浏览器中访问这个setmenu.php代码文件,方式和上面验证时填写URL时一样,然后你会得到返回码和一些粗略解释.
如果返回码是0,恭喜你设置成功,可以通过扫描接口测试号页面的二维码关注你的接口测试号查看效果
如果返回码是其它,请按后面的解释修改,如果还有疑惑可以查看微信的全局返回码说明,以后主动调用接口时的返回码都和这个一样

实现自动回复

首先我们来明确下一次与用户交互的过程:

  1. 用户关注公众号,或是点击菜单和发送消息在微信上与公众号交互
  2. 用户交互的结果将由微信客户端提交给微信服务器
  3. 微信服务器将信息形成xml形式的post表单,并发送给开发者服务器
  4. 开发者服务器根据post表单的内容进行处理,并将结果形成xml形式的get表单发送给微信服务器
  5. 微信服务器根据开发者服务器返回的表单将相应结果呈递给客户端

所以我们需要做的事就是接收post表单,并根据表单中的信息作出相应反馈即可.
为了程序的封装性,最好把处理过程都写在一个类里,并设置好权限和接口,由index.php直接调用即可.

首先定义一个类,用来保存要用到的所有方法
将以下代码保存为WechatCallbackAPI.php,保存在你想要的目录
新建的两个对象待会会有用

<?php
class WechatCallbackAPI(){
    //存放一些特殊的对象
    private $ToUserName, $FromUserName;
}
?>

接收post表单

php脚本会自动接收post表单,以供随时调用.所以我们只需要调用相应的方法就好了.
在刚才的WechatCallbackAPI类中新增一个私有方法,用来接收并处理表单:

//获取收到的post表单并转换为xml格式
private function getinfo()
{
    //获取原生的post表单
    $postStr = file_get_contents("php://input");
    if (empty($postStr)) {
        echo "GG";
        exit;
    }
    //要安装php7.0-xml扩展才能使用这个函数,用来将xml形式的字符串解释成键值对数组
    $res = simplexml_load_string($postStr, "SimpleXMLElement", LIBXML_NOCDATA);

    return $res;
}

当调用这个方法时,post表单就会被接收并初步处理转换为一个数组,并传回给另一个方法进行进一步处理

根据表单中的内容进行处理

获取了表单中的内容,就要对不同的内容进行不同的处理,以返回不同的结果
为此,我们需要进行以下几步操作:

  1. 判断表单中包含的数据类型
  2. 根据需要对不同类型的事件和关键词进行不同的处理
  3. 根据需要返回的信息和处理结果构建合理的get表单
  4. 将构建好的get表单返回给微信服务器

然后我们就可以一步步地进行处理了

判断表单中包含的数据类型并呈递给不同的方法处理

在刚才的创建的类中新建一个公开方法,并且代码如下:

//与index.php的接口
    public function handleMsg()
    {
        $postXml = $this->getinfo();    //调用刚才写的方法以获取表单
        $msgType = $postXml->MsgType;   //获取表单中表明的消息类型

        //先将待会发送的表单中要的两个信息保存好
        $this->ToUserName = $postXml->FromUserName;
        $this->FromUserName = $postXml->ToUserName;

        //目前只支持text型和event型,更多类型可以自己进行探索
        switch ($msgType) {
            case "text":    
            //将整个表单呈递给处理text型信息的方法处理,也即用户向微信公众号发送消息后的处理
                $this->handleTextMsg($postXml);
                exit;
            case "event":   
            //将整个表单呈递给处理event型信息的方法进行处理,也即用户点击微信公众号自定义菜单中的按钮后的处理
                $this->handleEventMsg($postXml);
                exit;
            default:
                echo "GG";
                exit;
        }
    }

额外说明:类中调用自己的元素或方法时加$this->才能正确调用,对于键值对数组,可以直接使用$array->key这种形式来获取数组中某个key对应的值

接着在类中写上对应的方法即可.先说说如何处理text型信息

//处理text型信息
    private function handleTextMsg($postXml)
    {
        //获取用户输入的消息,修剪一下去掉最后的换行符或空格
        $content = trim($postXml->Content);

        //根据不同的关键词进行处理
        //cmpStr是我自定义的一个方法,用来比较非英文字符串,解释在下面
        if ($this->cmpStr($content, "即使我死了,被钉在棺材里,也要用腐朽的声带喊出:")) {
            $returnStr = "吴东牛逼!";
            $MsgType = "text";
        } else if ($content == "testing english") {
            $returnStr = "success!";
            $MsgType = "text";
        } else {
            //如果不包含任何关键词当然也要回复,一般是回复"你的消息已收到之类的"
            $returnStr = $content;
            $MsgType = "text";
        }

        //这个方法用于构建并发送表单,之后讲完event型的处理之后再讲
        $this->constructAndSendMsg($MsgType, $returnStr);
    }

    //比较非英文字符串时需要用到,将两个字符串转换成相同编码比较,需要php7.0-mbstring扩展支持
    private function cmpStr($str1, $str2)
    {
        return (mb_convert_encoding($str1, "UTF-8") == mb_convert_encoding($str2, "UTF-8"));
    }

对于event型处理和对text型的处理差不多,不过event实际上有两个信息可供判断.

  • 一个是Event,是触发事件的触发器类型.比如subscribe表示用户关注订阅号时的事件,我们可以用它来发一些欢迎消息.
    再比如CLICK是用户点击菜单按钮时的事件.

  • 还有一个是EventKey,也就是我们在自定义菜单时填的key,可以用它来判断用户究竟是点了哪个按钮,以此进行特殊处理.

  • 更多类型的事件可查看微信的开发文档

下面这个私有方法就实现了对event型事件的处理示例.

//放在类中最顶部以供方便修改
private $welcomeMsg = "真实吴东,在线牛逼\n点击\"一键牛逼\",开启您的快捷mod东时代\n点击\"作者博客\",获取donglao小迷弟的博客地址\n点击\"获取图片\",一睹donglao尊容(由于我还想多活几年已被替换)";

//处理event型信息
    private function handleEventMsg($postXml)
    {
        $event = $postXml->Event;

        if ($event == "subscribe") {    //关注时触发的事件
            $returnStr = $this->welcomeMsg;
            $msgType = "text";
        } else if ($event == "CLICK") { //点击菜单上的按钮时的事件
            $content = $postXml->EventKey;
            switch ($content) {
                case "oneClickNB":
                    $returnStr = "吴东牛逼!";
                    $msgType = "text";
                    break;
                case "myBlog":
                    $returnStr = "http://elatis.cn/";
                    $msgType = "text";
                    break;
                default:
                    $returnStr = "UnkownEventKey:" . $content;
                    $msgType = "text";
            }
        } else {
            $returnStr = "UnkownEventType:" . $event;
            $msgType = "text";
        }

        $this->constructAndSendMsg($msgType, $returnStr);
    }

根据需求处理好用户发送过来的信息后,我们还需要构建好要发送回去的GET表单并进行发送.

构建并发送表单

根据开发文档中的说明,被动回复消息所发送给微信服务器的表单需要写成xml的形式,类似微信服务器发过来的效果.
由于是微信服务器主动调用我们的程序,所以也不需要进行什么额外的验证.
我先解释一下要发送的表单,以下是发送文件消息时的表单形式

<xml>
    <ToUserName><![CDATA[%s]]></ToUserName>
    <FromUserName><![CDATA[%s]]></FromUserName>
    <CreateTime>%s</CreateTime>
    <MsgType><![CDATA[%s]]></MsgType>
    <Content><![CDATA[%s]]></Content>
</xml>

参数解释:
ToUserName:要发送给的用户名,也就是微信服务器发过来的表单中的FromUserName
FromUserName:发送者的用户名,也就是微信服务器发过来的表单中的ToUserName
CreateTime:表单的创建时间,发送时获取一下系统时间即可.
MsgType:消息类型,如果是文字形式就是text,是图片形式就是image,当然还有一些别的形式
Content:表单包含的内容,其中的内容会以字符串的形式发送给客户.

可以直接将上面的表单以一个字符串的形式储存起来.然后将所有信息用sprintf方法写入表单.
由于微信服务器可以直接接收这个程序的返回值,所以直接echo一下就可以了

不过text型的信息和其它类型的信息的表单有些不一样,所以可以将所有类型的表单相同的部分储存到几个字符串中,将不同的部分额外处理,再把这些字符串拼接起来即可.
下面的代码就实现了构建和发送text型和image型表单的功能:

private function constructAndSendMsg($MsgType, $Content)
    {
        //将相同的部分储存到两个字符串中
        $textPartOne = "<xml>
                            <ToUserName><![CDATA[%s]]></ToUserName>
                            <FromUserName><![CDATA[%s]]></FromUserName>
                            <CreateTime>%s</CreateTime>
                            <MsgType><![CDATA[%s]]></MsgType>";
        $textPartTwo = "</xml>";

        if ($MsgType == "text") {   //处理text型
            $message = "<Content><![CDATA[%s]]></Content>";
        } else if ($MsgType == "image") {   //处理image型
            $textPartOne = $textPartOne . "<Image>";
            $textPartTwo = "</Image>" . $textPartTwo;
            $message = "<MediaId><![CDATA[%s]]></MediaId>";
        }

        //表单写入所需内容并拼接
        $textPartOne = $textPartOne . sprintf($message, $Content);
        $text = $textPartOne . $textPartTwo;
        //获取系统时间
        $time = time();
        $resMsg = sprintf($text, $this->ToUserName, $this->FromUserName, $time, $MsgType);

        //发送表单
        echo $resMsg;
    }

这样一个基本的处理逻辑就写好了,之后只要在index.php中实例化这个类,并调用这个类的handleMsg方法即可

<?php
//引用刚才写的代码
include "./src/php/WechatCallbackAPI.php";
//实例化这个类并调用
$wxObject = new WechatCallbackAPI();
$wxObject->handleMsg();
?>

到此为止的完整代码将随以下代码附在本篇最后

extra:如何上传图片

上传图片的说明可见微信的开发文档.这里我就简单介绍一下
上传图片和上传自定义菜单形式类似,不过上传自定义图片需要发送post表单.
所以一次上传图片的过程如下:

  1. 获取access_token
  2. 以post表单的形式向微信提供的接口发送图片(curl)
  3. 获取返回的内容写入文件储存

第一步使用我们之前写过的get_access_token.php即可
第二步应该也没什么问题,因为之前已经用curl处理过类似的问题了.只需要注意curl要获得一个文件使用的方法.
第三步可以用浏览器调用上传代码并保存浏览器中出现的数据来解决
所以可以写出以下代码,保存为uploadMedia.php:

<?php
include "get_access_token.php";

//在此填写所有素材的名字
$mediaNames = array("1.jpg", "2.jpg");
//选择是要临时素材接口还是永久素材接口
$tempMediaAPI = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=";
$eternalMediaAPI = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=";
//在此修改上传的数据类型
$mediaType = "image";

//获取access_token
$access_token = getAccessToken();
$fileName = "./src/logs/uploadMedia.json";
$mediaPaths = "./src/imgs/";
//上传所有图文消息需要的图片
foreach ($mediaNames as $value) {
    $media = $mediaPaths . $value;
    //使用curl上传图片
    $curl = curl_init();
    $url = $eternalMediaAPI . $access_token . "&type=" . $mediaType;
    curl_setopt($curl, CURLOPT_URL, $url);
    //按照要求,图片的键值为media
    curl_setopt($curl, CURLOPT_POSTFIELDS, [
        "media" => (new CURLFile(realpath($media))),
    ]);
    //还要接收返回的json数据

    curl_close($curl);
}
unset($value);

保存到你想要的位置之后在浏览器上访问该代码,浏览器上就会出现微信服务器返回的写成json形式的数据,保存好其中的media_id即可.
到此为止一个简易的微信公众号就搭建完毕了.希望这篇文章对大家有所帮助.

项目源码

点此下载

评论

发送失败 可能是您的发言太频繁或联系方式有误

提交评论

Theme LightWhite Made by Archeb With
自豪地使用Typecho
© 2017 - 2020 elatis.cn 版权所有 ICP证: 冀ICP备18008017号-1
全站共 19.11 W 字
博客已经运行了