一. 项目背景
1.公司想要为小程序开发一个 搜索widget 自定义模板. 即当用户使用微信客户端搜索小程序的时候,能达到下图中的效果

2.微信官方:https://developers.weixin.qq.com/miniprogram/introduction/widget/custom/有详细的操作步骤, 这里不过多的赘述.本文主要记叙涉及到的两个接口的实现: (1)导入抽样数据 (2)开发后台接口
二. 项目实现
1. 导入抽样数据
第一步, 获取access_token
public function getAccessToken(){ $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid='.$this->appid.'&secret='.$this->secret; $rs = ihttp_get($url); $ret = json_decode($rs['content'],true); return $ret['access_token'];}第二步, 根据不同的业务需求组建json, 这里以汽车出行服务为例

public function setDynaMicData() { $access_token = $this->getAccessToken(); $url = 'http://api.weixin.qq.com/wxa/setdynamicdata?access_token='.$access_token; $post['lifespan'] = 86400; $post['query'] = json_encode(array('type'=>1000013)); $post['scene'] = 1; $id = time(); $seq = 0; $totalCount = pdo_fetchcolumn('SELECT * FROM zc2_station_line WHERE acid='.$this->uniacid); for($count=0;$count<=$totalCount;$count+=5000) { $lineStations = pdo_fetchall("SELECT * FROM zc2_station_line WHERE acid={$this->uniacid} LIMIT {$count},5000"); foreach ($lineStations as $key=>$val) { $stations[$key]['from']['city'] = $val['StartStationName']; $stations[$key]['to']['city'] = $val['EndStationName']; } $attribute['count'] = count($lineStations); $attribute['totalcount'] = $totalCount; $attribute['id'] = "{$id}"; $attribute['seq'] = $seq; $items['attribute'] = $attribute; $post['data'] = '{"items":'.json_encode($stations).', "attribute":'.json_encode($attribute).'}'; $ret[] = ihttp_post($url, json_encode($post)); $seq++; } return $ret; }
2. 开发后台接口
后台接口指的是, 你的服务器与微信服务器的消息交互接口. 当用户搜索你的小程序时, 发现本地没有相关缓存或者缓存已经过期, 则请求这个接口, 返回数据给用户;比较麻烦的是消息的交互需要用进行验证, 但是, 微信也有各种语言版本的库提供;我这里是将所有加密解密文件封装成一个类, WXBizMsgCrypt.php(将在后面贴出);
第一步, 封装加密解密工具类WXBizMsgCrypt.php(在这里下载:https://wximg.gtimg.com/shake_tv/mpwiki/cryptoDemo.zip)
第二步, 利用加密解密工具进行验证, 验证通过后就可以自己的处理业务逻辑了getWidgetData()
三. 项目代码及调用示例
代码示例:
WXBizMsgCrypt.php<?php/** * 服务器与微信服务器通讯过程中的加密解密类 * 1.第三方回复加密消息给公众平台; * 2.第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。 * * @Author: xiewg * @since: 20190118 */class WXBizMsgCrypt{ private $token; private $encodingAesKey; private $appId; public $key; public $block_size = 32; /** * error code 说明. * <ul> * <li>-40001: 签名验证错误</li> * <li>-40002: xml解析失败</li> * <li>-40003: sha加密生成签名失败</li> * <li>-40004: encodingAesKey 非法</li> * <li>-40005: appid 校验错误</li> * <li>-40006: aes 加密失败</li> * <li>-40007: aes 解密失败</li> * <li>-40008: 解密后得到的buffer非法</li> * <li>-40009: base64加密失败</li> * <li>-40010: base64解密失败</li> * <li>-40011: 生成xml失败</li> * </ul> */ public static $OK = 0; public static $ValidateSignatureError = -40001; public static $ParseXmlError = -40002; public static $ComputeSignatureError = -40003; public static $IllegalAesKey = -40004; public static $ValidateAppidError = -40005; public static $EncryptAESError = -40006; public static $DecryptAESError = -40007; public static $IllegalBuffer = -40008; public static $EncodeBase64Error = -40009; public static $DecodeBase64Error = -40010; public static $GenReturnXmlError = -40011; /** * 构造函数 * @param $token string 公众平台上,开发者设置的token * @param $encodingAesKey string 公众平台上,开发者设置的EncodingAESKey * @param $appId string 公众平台的appId */ public function __construct($token, $encodingAesKey, $appId) { $this->token = $token; $this->encodingAesKey = $encodingAesKey; $this->appId = $appId; $this->key = base64_decode($encodingAesKey . "="); } /** * 将公众平台回复用户的消息加密打包. * <ol> * <li>对要发送的消息进行AES-CBC加密</li> * <li>生成安全签名</li> * <li>将消息密文和安全签名打包成xml格式</li> * </ol> * * @param $replyMsg string 公众平台待回复用户的消息,xml格式的字符串 * @param $timeStamp string 时间戳,可以自己生成,也可以用URL参数的timestamp * @param $nonce string 随机串,可以自己生成,也可以用URL参数的nonce * @param &$encryptMsg string 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, * 当return返回0时有效 * * @return int 成功0,失败返回对应的错误码 */ public function encryptMsg($replyMsg, $timeStamp, $nonce, &$encryptMsg) { //加密 $array = $this->encrypt($replyMsg, $this->appId); $ret = $array[0]; if ($ret != 0) { return $ret; } if ($timeStamp == null) { $timeStamp = time(); } $encrypt = $array[1]; //生成安全签名 $array = $this->getSHA1($this->token, $timeStamp, $nonce, $encrypt); $ret = $array[0]; if ($ret != 0) { return $ret; } $signature = $array[1]; $encryptMsg = array('encrypt'=>$encrypt, 'msgSignature'=>$signature, 'timeStamp'=>$timeStamp, 'nonce'=>$nonce); return self::$OK; } /** * 检验消息的真实性,并且获取解密后的明文. * <ol> * <li>利用收到的密文生成安全签名,进行签名验证</li> * <li>若验证通过,则提取xml中的加密消息</li> * <li>对消息进行解密</li> * </ol> * * @param $msgSignature string 签名串,对应URL参数的msg_signature * @param $timestamp string 时间戳 对应URL参数的timestamp * @param $nonce string 随机串,对应URL参数的nonce * @param $postData string 密文,对应POST请求的数据 * @param &$msg string 解密后的原文,当return返回0时有效 * * @return int 成功0,失败返回对应的错误码 */ public function decryptMsg($msgSignature, $timestamp = null, $nonce, $postData, &$msg) { if (strlen($this->encodingAesKey) != 43) { return self::$IllegalAesKey; } if ($timestamp == null) { $timestamp = time(); } $encrypt = $postData; //验证安全签名 $array = $this->getSHA1($this->token, $timestamp, $nonce, $encrypt); $ret = $array[0]; if ($ret != 0) { return $ret; } $signature = $array[1]; if ($signature != $msgSignature) { return self::$ValidateSignatureError; } $result = $this->decrypt($encrypt, $this->appId); if ($result[0] != 0) { return $result[0]; } $msg = $result[1]; return self::$OK; } /** * 用SHA1算法生成安全签名 * @param string $token 票据 * @param string $timestamp 时间戳 * @param string $nonce 随机字符串 * @param string $encrypt_msg 密文消息 * @return array */ public function getSHA1($token, $timestamp, $nonce, $encrypt_msg) { //排序 try { $array = array($encrypt_msg, $token, $timestamp, $nonce); sort($array, SORT_STRING); $str = implode($array); return array(self::$OK, sha1($str)); } catch (Exception $e) { //print $e . ""; return array(self::$ComputeSignatureError, null); } } /** * 对明文进行加密 * @param string $text 需要加密的明文 * @return string 加密后的密文 */ public function encrypt($text, $appid) { try { //获得16位随机字符串,填充到明文之前 $random = $this->getRandomStr(); $text = $random . pack("N", strlen($text)) . $text . $appid; // 网络字节序 $size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC); $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, ''); $iv = substr($this->key, 0, 16); //使用自定义的填充方式对明文进行补位填充 $text = $this->encode($text); mcrypt_generic_init($module, $this->key, $iv); //加密 $encrypted = mcrypt_generic($module, $text); mcrypt_generic_deinit($module); mcrypt_module_close($module); //print(base64_encode($encrypted)); //使用BASE64对加密后的字符串进行编码 return array(self::$OK, base64_encode($encrypted)); } catch (Exception $e) { //print $e; return array(self::$EncryptAESError, null); } } /** * 对密文进行解密 * @param string $encrypted 需要解密的密文 * @return string 解密得到的明文 */ public function decrypt($encrypted, $appid) { try { //使用BASE64对需要解密的字符串进行解码 $ciphertext_dec = base64_decode($encrypted); $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, ''); $iv = substr($this->key, 0, 16); mcrypt_generic_init($module, $this->key, $iv); //解密 $decrypted = mdecrypt_generic($module, $ciphertext_dec); mcrypt_generic_deinit($module); mcrypt_module_close($module); } catch (Exception $e) { return array(self::$DecryptAESError, null); } try { //去除补位字符 $result = $this->decode($decrypted); //去除16位随机字符串,网络字节序和AppId if (strlen($result) < 16) return ""; $content = substr($result, 16, strlen($result)); $len_list = unpack("N", substr($content, 0, 4)); $xml_len = $len_list[1]; $xml_content = substr($content, 4, $xml_len); $from_appid = substr($content, $xml_len + 4); } catch (Exception $e) { //print $e; return array(self::$IllegalBuffer, null); } if ($from_appid != $appid) return array(self::$ValidateAppidError, null); return array(0, $xml_content); } /** * 随机生成16位字符串 * @return string 生成的字符串 */ function getRandomStr() { $str = ""; $str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; $max = strlen($str_pol) - 1; for ($i = 0; $i < 16; $i++) { $str .= $str_pol[mt_rand(0, $max)]; } return $str; } /** * @param $text * @return string */ public function encode($text) { $block_size = $this->block_size; $text_length = strlen($text); //计算需要填充的位数 $amount_to_pad = $block_size - ($text_length % $block_size); if ($amount_to_pad == 0) { $amount_to_pad = $block_size; } //获得补位所用的字符 $pad_chr = chr($amount_to_pad); $tmp = ""; for ($index = 0; $index < $amount_to_pad; $index++) { $tmp .= $pad_chr; } return $text . $tmp; } /** * 对解密后的明文进行补位删除 * @param $text string decrypted解密后的明文 * @return string 删除填充补位后的明文 */ public function decode($text) { $pad = ord(substr($text, -1)); if ($pad < 1 || $pad > 32) { $pad = 0; } return substr($text, 0, (strlen($text) - $pad)); }}
widget.php
<?phpclass Widget{ private $uniacid = 0; private $appid = null; private $secret = null; private $signature = null; private $timestamp = null; private $nonce = null; private $token = null; private $encodingAesKey = null; public function __construct($_GPC) { $this->uniacid = 6; $this->appid = 'xxxxxx'; $this->secret = 'xxxxxx'; $this->token = 'xxxxxx'; $this->encodingAesKey = 'xxxxxxx'; $this->msgSignature = $_GPC['msg_signature']; $this->encrypt = $_GPC['__input']['Encrypt']; $this->signature = $_GPC['signature']; $this->timestamp = $_GPC['timestamp']; $this->nonce = $_GPC['nonce']; } /** * @Function: 小程序获取access_token * @return string */ public function getAccessToken() { $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid='.$this->appid.'&secret='.$this->secret; $rs = ihttp_get($url); $ret = json_decode($rs['content'],true); return $ret['access_token']; } /** * @Function: 微信widget搜索,提交数据接口 * @return array */ public function setDynaMicData() { $access_token = $this->getAccessToken(); $url = 'http://api.weixin.qq.com/wxa/setdynamicdata?access_token='.$access_token; $post['lifespan'] = 86400; $post['query'] = json_encode(array('type'=>1000013)); $post['scene'] = 1; $id = time(); $seq = 0; $totalCount = pdo_fetchcolumn('SELECT * FROM zc2_station_line WHERE acid='.$this->uniacid); for($count=0;$count<=$totalCount;$count+=5000) { $lineStations = pdo_fetchall("SELECT * FROM zc2_station_line WHERE acid={$this->uniacid} LIMIT {$count},5000"); foreach ($lineStations as $key=>$val) { $stations[$key]['from']['city'] = $val['StartStationName']; $stations[$key]['to']['city'] = $val['EndStationName']; } $attribute['count'] = count($lineStations); $attribute['totalcount'] = $totalCount; $attribute['id'] = "{$id}"; $attribute['seq'] = $seq; $items['attribute'] = $attribute; $post['data'] = '{"items":'.json_encode($stations).', "attribute":'.json_encode($attribute).'}'; $ret[] = ihttp_post($url, json_encode($post)); $seq++; } return $ret; } /** * 微信widget搜索,响应服务器与微信服务器消息交互接口 * * 微信将上述请求包通过http post方式发送开发者在mp平台上配置的URL * json格式 * POST * http://yourdomain/youdir?timestamp=XXX&nonce=XXX&msg_signature=XXX&signature=XXX * * @return */ public function getWidgetData() { $WXBizMsgCrypt = new WXBizMsgCrypt($this->token, $this->encodingAesKey, $this->appid); //收到公众号平台发送的消息,进行解密操作 $rec = ''; $errCode = $WXBizMsgCrypt->decryptMsg($this->msgSignature, $this->timestamp, $this->nonce, $this->encrypt, $rec); if ($errCode == 0)//解密成功,进一步处理业务逻辑 { $rec = json_decode($rec,true); $enTimeStamp = time(); $content = array(); $content['lifespan'] = 86400;//告诉微信此次数据可以缓存多久,单位为秒 $content['query'] = $rec['Query'];//微信服务器发来的query,原封不动填到这里,为json_encode之后的字符串 $content['scene'] = 1;//固定为1,表明来自搜索 $data['err_code'] = 0;//err_code中0代表正确,非0代表错误 $data['err_msg'] = 'success';//-1:无结果 -2:参数不对 -3:系统错误 $data['jump_url'] = '/pages/index/index';//跳转路径 $data['update_time'] = date('m月d日 D',$enTimeStamp);//更新时间 $data['more_description'] = '点击立即购票';//底部更多描述 //获取班次列表 $station_data_list = $this->getDepartures($content['query']); file_put_contents('test.txt',json_encode($station_data_list).PHP_EOL,FILE_APPEND); $data['station_data_list'] = $station_data_list; $content['data'] = json_encode($data); $post = array(); $post['ToUserName'] = $rec['FromUserName']; $post['FromUserName'] = $rec['ToUserName']; $post['CreateTime'] = $enTimeStamp; $post['MsgType'] = 'widget_data'; $post['Content'] = json_encode($content); $encryptMsg = ''; $nonce = '554484'; $errCode = $WXBizMsgCrypt->encryptMsg(json_encode($post), $enTimeStamp, $nonce, $encryptMsg); if ($errCode == 0) {//加密成功 return array('Encrypt'=>$encryptMsg['encrypt'], 'MsgSignature'=>$encryptMsg['msgSignature'], 'TimeStamp'=>$enTimeStamp, 'Nonce'=>$nonce); } else {//加密失败 $ret = array('err_code'=>-3,'err_msg'=>'加密失败'); return $ret; } } else {//解密失败 $ret = array('err_code'=>-3,'err_msg'=>'解密失败'); return $ret; } } /** * 检验signature * @param $signature * @param $timestamp * @param $nonce * @param $token * @return bool */ private function checkSignature($signature,$timestamp,$nonce,$token) { $tmpArr = array($token, $timestamp, $nonce); sort($tmpArr, SORT_STRING); $tmpStr = implode( $tmpArr ); $tmpStr = sha1( $tmpStr ); if ($tmpStr == $signature ) { return true; } else { return false; } } /** * 查询班次(本项目的业务逻辑) * @param $query * @return array */ public function getDepartures($query) { file_put_contents('test.txt',$query.PHP_EOL,FILE_APPEND);//测试 $station_data_list = array(); $url = 'https://zc2api.zhongchengbus.cn/?s=Kgkx.TicketApi.departures&appid=wxapp_kgkx&nonce=73×tamp=1548049804&signature=bd7a83fdd15a3035e90b64040eb7e518188f51f3'; $query = json_decode($query,true); foreach ($query['station_list'] as $item) { file_put_contents('test.txt',"SELECT * FROM zc2_station_line WHERE sch_station_name='{$item['from_city']}' AND sch_dst_name='{$item['to_city']}' AND acid={$this->uniacid}".PHP_EOL,FILE_APPEND);//测试 $get_station_res = pdo_fetch("SELECT * FROM zc2_station_line WHERE sch_station_name='{$item['from_city']}' AND sch_dst_name='{$item['to_city']}' AND acid={$this->uniacid}"); $StartStation = $get_station_res['sch_station_code']; $EndStation = $get_station_res['sch_dst_node']; $SchDate = $item['date']; file_put_contents('test.txt',json_encode($get_station_res).PHP_EOL,FILE_APPEND); $params = array('StartStation'=>$StartStation,'EndStation'=>$EndStation,'SchDate'=>$SchDate); $res = ihttp_post($url,$params); file_put_contents('test.txt',json_encode($params).PHP_EOL,FILE_APPEND); file_put_contents('test.txt',$res['content'].PHP_EOL,FILE_APPEND); $content = json_decode($res['content'],true); if($content['ret'] == '200') { foreach ($content['data'] as $val) { $station_data_list_tmp['from'] = $val['SchWaitStName'];//|string|起始地|广东省汽车客运站| $station_data_list_tmp['from_time'] = $val['SchTime'];//|string|起飞时间|13:40| $station_data_list_tmp['to'] = $val['SchNodeName'];//|string|目的地|武汉路口| $station_data_list_tmp['to_time'] = '';//|string|到达时间|01:40| $station_data_list_tmp['special'] = '';//|string|跨天|+1天| $station_data_list_tmp['tips'] = '抢票';//|string|提示|抢票| $station_data_list_tmp['icon'] = '';//|string|图标|https://xxx| $station_data_list[] = $station_data_list_tmp; } } } return $station_data_list; }}
调用示例:
调用导入数据接口:
$Widget = new Widget($_GPC);$res = $Widget->setDynaMicData();echo $res;exit();调用后台接口:
$Widget = new Widget($_GPC);$res = $Widget->getWidgetData();echo $res;exit();













