做网课查题之类的公众号越来越多,搭建免费查题服务是个不错的引流方法,难点在于接口的对接。人数少的话使用付费接口并不划算,然而免费接口含有广告,并且也经常抽风,这篇教程就要解决这些问题,搭建稳定的查题服务。
关注“自主查题助手”可以免费查题,给我涨波粉吧^-^
思路讲解
- 在微信公众平台填写自己搭建的php文件的url。
- 这个php文件接收用户通过公众号发送的消息,再模仿微信的请求将其转发给免费或付费查题接口,然后接受返回来的题目和答案,回复给微信。
- 简单的说就是自己做一个接口充当中间人负责转发消息。这样就可以修改查题接口返回的信息了。
- 还可以多找几个接口,当其中一个无法访问时,转发给下一个接口接着查,保证稳定性。
- 甚至还可以拓展业务,分析用户关键词等消息,来执行不同的逻辑。
公众号申请
这个不是重点,进入微信公众平台注册一个订阅号即可。如有营业执照等相关材料,可以注册服务号,功能更多。
对接接口
1.基本配置
登录公众平台,选择左下角开发中的基本配置,公众号开发信息暂时不用管,看下面的服务器配置
这里的服务器地址填写自己将要做接口使用的php文件路径。php代码的编写在下一标题。注意:php文件写完了才能回来填写这个服务器地址
令牌填写随意一个字符串,一会二微信验证接口合法性的时候会使用该字符串来验证。
为方便处理,消息加解密方式使用明文模式。
2.免费接口
我这里使用的是搜题吧的免费接口。既然白嫖了接口,就在这宣传一波。
注册账号登录后可见以下页面。
如果你只是想做个简简单单的公众号,那就将接口地址(URL)和令牌(TOKEN)填入微信公众平台的接口地址和令牌中就OK了。免费的接口会在返回的信息最后面加上“戳这:<a href=”h**ps://api.soutiba.cn”>免费接入查题<a> ”。并且免费接口会不定期无法访问。
我们的目的有:去除这个末尾的广告,在接口无法访问时请求其他的接口确保得到题目答案。
这样做还有一个好处,那就是可以在返回给用户的消息末尾加上自己想要的内容(比如“回复【菜单】即可查看菜单”),为向用户推广特定内容做准备。
接口搭建
1.接口验证
按照在公众平台中填写的URL地址,在服务器新建php文件。
是首先要做的是接口的验证。前面讲过,提交服务器地址的时候会使用Token来验证接口合法性。因此要先编写代码来处理来自微信的验证请求。
这段代码百度有很多,这里放上我测试能用的
<?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(); ?>
记得将
$token
换成自己的Token。对照公众平台的开发者文档,不难理解其逻辑。不理解也没问题,这里只要能验证就行。php文件保存好后,回到公众平台,填写php文件的url地址以及对应的token,提交时会自动验证。若配置成功,则表示代码生效。
回复逻辑代码的编写一种方法是在
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();
这是php的入口程序,查看公众平台的开发文档可以得知,微信的请求链接中含有
$timestamp
,$nonce
,$signature
,分别取出这些参数,待会儿使用。请求题含有用户发送的消息,使用下面的语句来获取
$postStr = file_get_contents("php://input");
消息是XML结构,使用下列语句来解析其中的属性。
$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA); $fromUsername = $postObj->FromUserName; $msgtype = $postObj->MsgType;
通过判断
$msgtype
来作出不同的响应。如关注判断,语音消息,图片消息,文本消息等。最后如果是文本消息则调用函数
soutiba()
,返回值为true
或false
,如果失败,调用repairing()
回复用户“接口维护中”。
2)soutiba()
function soutiba($signature, $timestamp, $nonce, $fromUsername, $postStr){
$url = "http://api.soutiba.cn/stage/weiapi/index?userid=*******************************&signature=".$signature."×tamp=".$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;
}
- 这就是请求查题接口的函数了,其实就是一个curl请求。
- 分析微信的请求链接格式,拼接这些参数,然后请求
http://api.soutiba.cn/stage/weiapi/index
,注意userid
更换成自己的搜题吧帐号首页显示的userid
。 - 由于免费接口可能会请求失败,因此需要设置超时时间,否则微信等待5s没有回复会显示“公众号故障”。
curl_setopt($ch, CURLOPT_TIMEOUT, 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;
}
- 主要作用就是查题接口请求失败的时候直接回复用户“更新题库”的消息。
3.消息处理升级版
前面说了,当接口无法访问的时候,就没法查题了,因此再找一个查题接口来,当第一个接口失效的时候请求第二个接口。
1)寻找接口
- 找了好久也没找到好用的,干脆找个网页查题的,分析一下,提取接口吧。找到的是这个http://jk.fm210.cn 网页,试了一下好像题库还行,都能查到,响应也比较快,而且网页端是没有限制的。如果注册账号的话,只有100次查询的机会,当然也可以购买,1000次/1元。
- Fiddle抓包得到请求:
http://jk.fm210.cn/web.php
,使用post
方式,参数如下: - 注意的是这个token是会变化的,查看网页源代码发现
token
由widow.jjm()
函数计算得到。 - 在浏览器的
console
窗口输入函数名window.jjm
即可定位到函数定义处。 - 不看不知道,一看吓一跳,代码用了
sojson.v5
加密。这个目前好像还没有高效的解密方法。但是不影响我们找出计算规则。毕竟代码怎么加密也都是要给浏览器执行的。 - 盯着看了一会儿,看出来了,就是简单地计算question+给定的一组随机字符串+fm的md5值。
- 解决办法有了,先请求网页
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;
}
- 先请求
http://jk.fm210.cn
得到随机字符串,再计算token
值,带上用户发送的题目,请求http://jk.fm210.cn/web.php
得到返回数据。 - 返回的数据是个
json
,tm
是题目,da
是答案。再将其返回给用户即可。
3)调用新接口
if(soutiba($signature, $timestamp, $nonce, $fromUsername, $postStr))
{
}
elseif(jkfm210cn($postObj))
{
}
else
{
repairing($postObj);
}
至此,接口代码编写完毕。
4.优化
搭建过程中的调试可以用
file_put_contents
将数据写入文本文件来查看执行效果。receive.txt
为文件名,$data
为要记录的变量名。file_put_contents("receive.txt", print_r($data, true));
当公众号上线后,不想停掉服务,但是又要增加或修改功能,可以加个判断,识别自己的微信账号,然后单独做出相应,用于测试新功能。
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'){ // 关注判断 }
结语
- 花钱买便利,白嫖看技术。
- 肝这么多代码,目的是白嫖吗?并不全是。能够自主请求查题接口,这样做等于预留了一个对接其他接口进行功能拓展的位置。为以后丰富功能做准备。
- 放个成果图
附:完整代码
<?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."×tamp=".$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();
?>