查题公众号搭建


做网课查题之类的公众号越来越多,搭建免费查题服务是个不错的引流方法,难点在于接口的对接。人数少的话使用付费接口并不划算,然而免费接口含有广告,并且也经常抽风,这篇教程就要解决这些问题,搭建稳定的查题服务。

关注“自主查题助手”可以免费查题,给我涨波粉吧^-^

思路讲解

  1. 在微信公众平台填写自己搭建的php文件的url。
  2. 这个php文件接收用户通过公众号发送的消息,再模仿微信的请求将其转发给免费或付费查题接口,然后接受返回来的题目和答案,回复给微信。
  3. 简单的说就是自己做一个接口充当中间人负责转发消息。这样就可以修改查题接口返回的信息了。
  4. 还可以多找几个接口,当其中一个无法访问时,转发给下一个接口接着查,保证稳定性。
  5. 甚至还可以拓展业务,分析用户关键词等消息,来执行不同的逻辑。

公众号申请

这个不是重点,进入微信公众平台注册一个订阅号即可。如有营业执照等相关材料,可以注册服务号,功能更多。

对接接口

1.基本配置

  1. 登录公众平台,选择左下角开发中的基本配置公众号开发信息暂时不用管,看下面的服务器配置
    服务器配置

  2. 这里的服务器地址填写自己将要做接口使用的php文件路径。php代码的编写在下一标题。注意:php文件写完了才能回来填写这个服务器地址

  3. 令牌填写随意一个字符串,一会二微信验证接口合法性的时候会使用该字符串来验证。

  4. 为方便处理,消息加解密方式使用明文模式

2.免费接口

  1. 我这里使用的是搜题吧的免费接口。既然白嫖了接口,就在这宣传一波。

  2. 注册账号登录后可见以下页面。
    查题接口

  3. 如果你只是想做个简简单单的公众号,那就将接口地址(URL)令牌(TOKEN)填入微信公众平台的接口地址令牌中就OK了。免费的接口会在返回的信息最后面加上“戳这:<a href=”h**ps://api.soutiba.cn”>免费接入查题<a> ”。并且免费接口会不定期无法访问。

  4. 我们的目的有:去除这个末尾的广告,在接口无法访问时请求其他的接口确保得到题目答案。

  5. 这样做还有一个好处,那就是可以在返回给用户的消息末尾加上自己想要的内容(比如“回复【菜单】即可查看菜单”),为向用户推广特定内容做准备。

接口搭建

1.接口验证

  1. 按照在公众平台中填写的URL地址,在服务器新建php文件。

  2. 是首先要做的是接口的验证。前面讲过,提交服务器地址的时候会使用Token来验证接口合法性。因此要先编写代码来处理来自微信的验证请求。

  3. 这段代码百度有很多,这里放上我测试能用的

    <?php
    function index()
    {
       $timestamp = $_GET['timestamp'];
       $nonce = $_GET['nonce'];
       $token = '**********';         //公众号里面配置的token
       $signature = $_GET['signature'];
       $echostr = $_GET['echostr'];   
    
       $array = array( $timestamp, $nonce, $token);
       sort( $array );
    
       //2.将排序后的三个参数拼接之后用sha1加密
       $tempstr = implode('', $array);
       $tempstr = sha1( $tempstr );
    
       //3.将加密后的字符串与signature进行对比,判断该请求是否来自微信
       if( $tempstr == $signature && $echostr){       //启动服务器配置 会进入到这里
               echo $_GET['echostr'];
               exit();
       }
    }
    index();
    ?>
  4. 记得将$token换成自己的Token。对照公众平台的开发者文档,不难理解其逻辑。不理解也没问题,这里只要能验证就行。

  5. php文件保存好后,回到公众平台,填写php文件的url地址以及对应的token,提交时会自动验证。若配置成功,则表示代码生效。

  6. 回复逻辑代码的编写一种方法是在index()函数最后的if块后面加上else块,跳转到消息处理函数。这样总感觉index()中的验证会消耗一定时间。因此我选择了另一种做法:保留index()函数体,注释掉其调用语句index();,直接进行消息处理。这样做的话,如果公众平台里面更改接口之后,重新验证的话,需要取消注释,调用index()进行验证。

2.消息处理基础版

1)主程序

function testWeixin(){
    $timestamp = $_GET['timestamp'];
    $nonce = $_GET['nonce'];
    $signature = $_GET['signature'];
    $echostr = $_GET['echostr'];

    $postStr = file_get_contents("php://input");
    $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
    $fromUsername = $postObj->FromUserName;
    $msgtype = $postObj->MsgType;

    if($msgtype=='event'){                 // 关注判断
        $toUsername = $postObj->ToUserName;
        $time = time();
        $textTpl = "<xml>
                      <ToUserName><![CDATA[%s]]></ToUserName>
                      <FromUserName><![CDATA[%s]]></FromUserName>
                      <CreateTime>%s</CreateTime>
                      <MsgType><![CDATA[%s]]></MsgType>
                      <Content><![CDATA[%s]]></Content>
                    </xml>";

        $msgType = "text";
        $contentStr = "你好呀,欢迎关注!😘\n\n向我发送题目即可返回参考答案。\n\n💡查题技巧:发送题目关键文字,不要带题目序号,尽量带少的标点符号,不要带特殊字符。\n若题目匹配不正确,请尝试带上选项发送\n\n👉 开始查题吧~";
        $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
        echo $resultStr;
    }
    //图片语音消息判断
    else if($msgtype=='image' || $msgtype == 'voice'){
        $toUsername = $postObj->ToUserName;
        $time = time();
        $textTpl = "<xml>
                      <ToUserName><![CDATA[%s]]></ToUserName>
                      <FromUserName><![CDATA[%s]]></FromUserName>
                      <CreateTime>%s</CreateTime>
                      <MsgType><![CDATA[%s]]></MsgType>
                      <Content><![CDATA[%s]]></Content>
                    </xml>";

        $msgType = "text";
        $contentStr = "暂不支持图片和语音格式的题目哦~";
        $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
        echo $resultStr;
    }
    else
    {
        if(soutiba($signature, $timestamp, $nonce, $fromUsername, $postStr))
        {

        }
        else
        {
            repairing($postObj);
        }
    }
}
testWeixin();
  1. 这是php的入口程序,查看公众平台的开发文档可以得知,微信的请求链接中含有$timestamp$nonce$signature,分别取出这些参数,待会儿使用。

  2. 请求题含有用户发送的消息,使用下面的语句来获取

    $postStr = file_get_contents("php://input");

    消息是XML结构,使用下列语句来解析其中的属性。

    $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
    $fromUsername = $postObj->FromUserName;
    $msgtype = $postObj->MsgType;
  3. 通过判断$msgtype来作出不同的响应。如关注判断,语音消息,图片消息,文本消息等。

  4. 最后如果是文本消息则调用函数soutiba(),返回值为truefalse,如果失败,调用repairing()回复用户“接口维护中”。

2)soutiba()

function soutiba($signature, $timestamp, $nonce, $fromUsername, $postStr){
    $url = "http://api.soutiba.cn/stage/weiapi/index?userid=*******************************&signature=".$signature."&timestamp=".$timestamp."&nonce=".$nonce."&openid=".$fromUsername;

    $ch = curl_init($url);    
    curl_setopt($ch, CURLOPT_HEADER, 0);    
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
    curl_setopt($ch, CURLOPT_POST, 1);    
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postStr);  
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($ch, CURLOPT_TIMEOUT, 4);
    $data = curl_exec($ch);
    curl_close($ch);
    if(strpos($data, "<ToUserName>"))
    {
        $data = getStrBeside($data, "\n戳这", "</a>");  
        echo $data;
        return true;
    }
    else
        return false;
}
  1. 这就是请求查题接口的函数了,其实就是一个curl请求。
  2. 分析微信的请求链接格式,拼接这些参数,然后请求http://api.soutiba.cn/stage/weiapi/index,注意userid更换成自己的搜题吧帐号首页显示的userid
  3. 由于免费接口可能会请求失败,因此需要设置超时时间,否则微信等待5s没有回复会显示“公众号故障”。
    curl_setopt($ch, CURLOPT_TIMEOUT, 4);
  4. getStrBeside()函数作用就是去除消息结尾的广告。传入参数依次为:完整字符串、广告头匹配字符串(\n戳这)、广告尾匹配字符串()。
    function getStrBeside($kw1,$stWord,$edWord)
    {
     $kw=$kw1;
     $st=stripos($kw,$stWord);
     $ed=stripos($kw,$edWord);
     if(($st==false||$ed==false)||$st>=$ed)
     return $kw1;
     $kw=substr($kw,0,($st))."".substr($kw,($ed+strlen($edWord)));
     return $kw;
    }

3)repairing()

function repairing($postObj){
    $fromUsername = $postObj->FromUserName;
    $toUsername = $postObj->ToUserName;
    $time = time();
    $textTpl = "<xml>
                  <ToUserName><![CDATA[%s]]></ToUserName>
                  <FromUserName><![CDATA[%s]]></FromUserName>
                  <CreateTime>%s</CreateTime>
                  <MsgType><![CDATA[%s]]></MsgType>
                  <Content><![CDATA[%s]]></Content>
                </xml>";

    $msgType = "text";
    $contentStr = "正在更新题库,请1-2小时后稍后再试。敬请谅解~";
    $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
    echo $resultStr;
}
  1. 主要作用就是查题接口请求失败的时候直接回复用户“更新题库”的消息。

3.消息处理升级版

前面说了,当接口无法访问的时候,就没法查题了,因此再找一个查题接口来,当第一个接口失效的时候请求第二个接口。

1)寻找接口

  1. 找了好久也没找到好用的,干脆找个网页查题的,分析一下,提取接口吧。找到的是这个http://jk.fm210.cn 网页,试了一下好像题库还行,都能查到,响应也比较快,而且网页端是没有限制的。如果注册账号的话,只有100次查询的机会,当然也可以购买,1000次/1元。
  2. Fiddle抓包得到请求:http://jk.fm210.cn/web.php,使用post方式,参数如下:
    抓到的数据
  3. 注意的是这个token是会变化的,查看网页源代码发现tokenwidow.jjm()函数计算得到。
    网页源码
  4. 在浏览器的console窗口输入函数名window.jjm即可定位到函数定义处。
    查找函数位置
  5. 不看不知道,一看吓一跳,代码用了sojson.v5加密。这个目前好像还没有高效的解密方法。但是不影响我们找出计算规则。毕竟代码怎么加密也都是要给浏览器执行的。
    函数位置
  6. 盯着看了一会儿,看出来了,就是简单地计算question+给定的一组随机字符串+fm的md5值。
  7. 解决办法有了,先请求网页http://jk.fm210.cn/,解析其中包含的一组随机字符串,即var token=window.jjm(question,"******");中星号代表的随机字符串。然后就直接将用户的消息随机字符串fm拼接起来计算md5作为token,再构造参数请求接口即可。

2)实现代码

function jkfm210cn($postObj){
    $fromUsername = $postObj->FromUserName;
    $toUsername = $postObj->ToUserName;
    $content = $postObj->Content;
    $url = "http://jk.fm210.cn/";
    $ch = curl_init($url);    
    curl_setopt($ch, CURLOPT_HEADER, 0);    
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($ch, CURLOPT_TIMEOUT, 4);
    $data = curl_exec($ch);
    curl_close($ch);
    $start = strpos($data,"window.jjm(question,");
    $token = md5($content."".substr($data, $start+21, 32)."fm");
    $url = "http://jk.fm210.cn/web.php";
    $postStr = "token=".$token."&type=2&question=".$content;
    $ch = curl_init($url);    
    curl_setopt($ch, CURLOPT_HEADER, 0);    
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
    curl_setopt($ch, CURLOPT_POST, 1);    
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postStr);  
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($ch, CURLOPT_TIMEOUT, 4);
    $data = json_decode(curl_exec($ch), true);
    curl_close($ch);

    $time = time();
    $msgType = "text";
    $textTpl = "<xml>
                  <ToUserName><![CDATA[%s]]></ToUserName>
                  <FromUserName><![CDATA[%s]]></FromUserName>
                  <CreateTime>%s</CreateTime>
                  <MsgType><![CDATA[%s]]></MsgType>
                  <Content><![CDATA[%s]]></Content>
                </xml>";
    if($data["da"] && $data["tm"]) {
        $contentStr = "🔒:".$data["tm"]."\n\n🔑:".$data["da"]."\n\n---  🔐 来自备用题库  ---";
    } else {
        $contentStr = "暂未找到,请尝试输入题目前部分文字(不要带序号、选项等题目以外内容)\n\n---  🔐 来自备用题库  ---";
    }
    $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
    echo $resultStr;
    return true;
}
  1. 先请求http://jk.fm210.cn得到随机字符串,再计算token值,带上用户发送的题目,请求http://jk.fm210.cn/web.php得到返回数据。
  2. 返回的数据是个jsontm是题目,da是答案。再将其返回给用户即可。

3)调用新接口

if(soutiba($signature, $timestamp, $nonce, $fromUsername, $postStr))
{
}
elseif(jkfm210cn($postObj))
{
}
else
{
    repairing($postObj);
}

至此,接口代码编写完毕。

4.优化

  1. 搭建过程中的调试可以用file_put_contents将数据写入文本文件来查看执行效果。receive.txt为文件名,$data为要记录的变量名。

    file_put_contents("receive.txt", print_r($data, true));
  2. 当公众号上线后,不想停掉服务,但是又要增加或修改功能,可以加个判断,识别自己的微信账号,然后单独做出相应,用于测试新功能。fromUsername通过file_put_contents记录来自微信的请求体,可以从中得到自己的微信”标识码”。

    if($fromUsername == "oknar1Gtu******************")
    {
       $content = $postObj->Content;
       $toUsername = $postObj->ToUserName;
       $time = time();
       $textTpl = "<xml>
                     <ToUserName><![CDATA[%s]]></ToUserName>
                     <FromUserName><![CDATA[%s]]></FromUserName>
                     <CreateTime>%s</CreateTime>
                     <MsgType><![CDATA[%s]]></MsgType>
                     <Content><![CDATA[%s]]></Content>
                   </xml>";
       $msgType = "text";
    
       $contentStr = "更多操作请回复——【帮助】";
       $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
       echo $resultStr;
       return;
    }
    if($msgtype=='event'){        // 关注判断
    }

结语

  1. 花钱买便利,白嫖看技术。
  2. 肝这么多代码,目的是白嫖吗?并不全是。能够自主请求查题接口,这样做等于预留了一个对接其他接口进行功能拓展的位置。为以后丰富功能做准备。
  3. 放个成果图
    查题

附:完整代码

<?php
    function index()
    {
        $timestamp = $_GET['timestamp'];
        $nonce = $_GET['nonce'];
        $token = '********';        //公众号里面配置的token
        $signature = $_GET['signature'];
        $echostr = $_GET['echostr'];   

        $array = array( $timestamp, $nonce, $token);
        sort( $array );

        //2.将排序后的三个参数拼接之后用sha1加密
        $tempstr = implode('', $array);
        $tempstr = sha1( $tempstr );

        //3.将加密后的字符串与signature进行对比,判断该请求是否来自微信
        if( $tempstr == $signature && $echostr){       //启动服务器配置 会进入到这里
                echo $_GET['echostr'];
                exit();
        }
    }

    function getStrBeside($kw1,$stWord,$edWord) {
        $kw=$kw1;
        $st=stripos($kw,$stWord);
        $ed=stripos($kw,$edWord);
        if(($st==false||$ed==false)||$st>=$ed)
        return $kw1;
        $kw=substr($kw,0,($st))."".substr($kw,($ed+strlen($edWord)));
        return $kw;
    }

    function soutiba($signature, $timestamp, $nonce, $fromUsername, $postStr) {
        $url = "http://api.soutiba.cn/stage/weiapi/index?userid=*******************&signature=".$signature."&timestamp=".$timestamp."&nonce=".$nonce."&openid=".$fromUsername;

        $ch = curl_init($url);    
        curl_setopt($ch, CURLOPT_HEADER, 0);    
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
        curl_setopt($ch, CURLOPT_POST, 1);    
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postStr);  
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
        curl_setopt($ch, CURLOPT_TIMEOUT, 4);
        $data = curl_exec($ch);
        curl_close($ch);
        if(strpos($data, "<ToUserName>"))
        {
            $data = getStrBeside($data, "\n戳这", "</a>");  
            echo $data;
            return true;
        }
        else
            return false;
    }

    function jkfm210cn($postObj) {
        $fromUsername = $postObj->FromUserName;
        $toUsername = $postObj->ToUserName;
        $content = $postObj->Content;
        $url = "http://jk.fm210.cn/";
        $ch = curl_init($url);    
        curl_setopt($ch, CURLOPT_HEADER, 0);    
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
        curl_setopt($ch, CURLOPT_TIMEOUT, 4);
        $data = curl_exec($ch);
        curl_close($ch);
        $start = strpos($data,"window.jjm(question,");
        $token = md5($content."".substr($data, $start+21, 32)."fm");
        $url = "http://jk.fm210.cn/web.php";
        $postStr = "token=".$token."&type=2&question=".$content;
        $ch = curl_init($url);    
        curl_setopt($ch, CURLOPT_HEADER, 0);    
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
        curl_setopt($ch, CURLOPT_POST, 1);    
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postStr);  
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
        curl_setopt($ch, CURLOPT_TIMEOUT, 4);
        $data = json_decode(curl_exec($ch), true);
        curl_close($ch);

        $time = time();
        $msgType = "text";
        $textTpl = "<xml>
                      <ToUserName><![CDATA[%s]]></ToUserName>
                      <FromUserName><![CDATA[%s]]></FromUserName>
                      <CreateTime>%s</CreateTime>
                      <MsgType><![CDATA[%s]]></MsgType>
                      <Content><![CDATA[%s]]></Content>
                    </xml>";
        if($data["da"] && $data["tm"]) {
            $contentStr = "🔒:".$data["tm"]."\n\n🔑:".$data["da"]."\n\n---  🔐 来自备用题库  ---";
        } else {
            $contentStr = "暂未找到,请尝试输入题目前部分文字(不要带序号、选项等题目以外内容)\n\n---  🔐 来自备用题库  ---";
        }
        $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
        echo $resultStr;
        return true;
    }
    function repairing($postObj){
        $fromUsername = $postObj->FromUserName;
        $toUsername = $postObj->ToUserName;
        $time = time();
        $textTpl = "<xml>
                      <ToUserName><![CDATA[%s]]></ToUserName>
                      <FromUserName><![CDATA[%s]]></FromUserName>
                      <CreateTime>%s</CreateTime>
                      <MsgType><![CDATA[%s]]></MsgType>
                      <Content><![CDATA[%s]]></Content>
                    </xml>";

        $msgType = "text";
        $contentStr = "正在更新题库,请1-2小时后稍后再试。敬请谅解~";
        $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
        echo $resultStr;
    }
    function testWeixin(){
        $timestamp = $_GET['timestamp'];
        $nonce = $_GET['nonce'];
        $signature = $_GET['signature'];
        $echostr = $_GET['echostr'];

        $postStr = file_get_contents("php://input");
        $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
        $fromUsername = $postObj->FromUserName;
        $msgtype = $postObj->MsgType;

        //来自tianqiraf的消息
        if($fromUsername == "oknar1GtuK1Q***********")
        {
            $content = $postObj->Content;
            $toUsername = $postObj->ToUserName;
            $time = time();
            $textTpl = "<xml>
                          <ToUserName><![CDATA[%s]]></ToUserName>
                          <FromUserName><![CDATA[%s]]></FromUserName>
                          <CreateTime>%s</CreateTime>
                          <MsgType><![CDATA[%s]]></MsgType>
                          <Content><![CDATA[%s]]></Content>
                        </xml>";
            $msgType = "text";
            $contentStr = $contentStr."\n\n更多操作请回复——【帮助】";
            $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
            echo $resultStr;
            return;
        }

        if($msgtype=='event'){               // 关注判断
            $toUsername = $postObj->ToUserName;
            $time = time();
            $textTpl = "<xml>
                          <ToUserName><![CDATA[%s]]></ToUserName>
                          <FromUserName><![CDATA[%s]]></FromUserName>
                          <CreateTime>%s</CreateTime>
                          <MsgType><![CDATA[%s]]></MsgType>
                          <Content><![CDATA[%s]]></Content>
                        </xml>";

            $msgType = "text";
            $contentStr = "你好呀,欢迎关注!😘\n\n向我发送题目即可返回参考答案。\n\n💡查题技巧:发送题目关键文字,不要带题目序号,尽量带少的标点符号,不要带特殊字符。\n若题目匹配不正确,请尝试带上选项发送\n\n👉 开始查题吧~";
            $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
            echo $resultStr;
        }
        //图片语音消息判断
        else if($msgtype=='image' || $msgtype == 'voice'){
            $toUsername = $postObj->ToUserName;
            $time = time();
            $textTpl = "<xml>
                          <ToUserName><![CDATA[%s]]></ToUserName>
                          <FromUserName><![CDATA[%s]]></FromUserName>
                          <CreateTime>%s</CreateTime>
                          <MsgType><![CDATA[%s]]></MsgType>
                          <Content><![CDATA[%s]]></Content>
                        </xml>";

            $msgType = "text";
            $contentStr = "暂不支持图片和语音格式的题目哦~";
            $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
            echo $resultStr;
        }
        else
        {
            if(soutiba($signature, $timestamp, $nonce, $fromUsername, $postStr))
            {
            }
            elseif(jkfm210cn($postObj))
            {
            }
            else
            {
                repairing($postObj);
            }
        }
    }
    //index();
    testWeixin();
?>

文章作者: Tqraf
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Tqraf !
评论
  目录