微信开发常用接口的PHP封装

微信接口这块的东西不是弄起来不算特别难,封装好了基本都能复用,不过没做过微信开发的同学第一次整起来还是显得有些蛋疼的。

常用的开发主要分4块
1.access_token的获取和存放:获取access_token的接口是有调用频率的限制的,我们不能每次需要的时候都去调用接口,我们可以将其写入文件,或者写入缓存。access_token的有效期目前为2个小时,每次拿的时候先将我们存放的时间和当前时间作对比,超过7200秒才去请求接口,否则直接拿文件或缓存里面存放的access_token

2.JS-SDK:这块主要是应用于微信的网页开发,可以直接使用微信提供的包括扫描二维码,上传图片,分享,定位等很多接口。具体可见JS-SDK文档和官方demo。所有需要使用JS-SDK的页面必须先注入配置信息,获取配置信息的接口,需要一个jsapi_ticket,和access_token一样,有效期也是2个小时,这里我们要做的也是存放jsapi_ticket和拿取配置信息
注意:这里需要到微信公众平台里面设置我们的JS接口安全域名,否则是用不了的

3.网页授权:这里主要是网页授权获取用户信息,然后插入或者更新的user表

  • 引导用户进入授权页面同意授权,获取code
  • 通过code换取网页授权access_token(与基础支持中的access_token不同)
  • 如果需要,开发者可以刷新网页授权access_token,避免过期
  • 通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)

注意:这里需要到微信公众平台里面设置我们网页授权域名

4.服务器配置:这里主要是作为一个回调,可以监听到用户的事件,比如关注,取关,click事件,接受用户发送的信息,这样我们可以跟各种事件或者信息来进行我们的逻辑

代码如下

1.access_token的获取和存放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php 

class Token
{

/**
* 微信配置信息
* @var array
*/

private function createAccessToken()
{
//微信配置
$appid = Yii::$app->params['WeChat']['AppID'];
$corpsecret = Yii::$app->params['WeChat']['AppSecret'];

$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$appid&secret=$corpsecret";
$accessTokenJSON = file_get_contents($url);
$accessTokenArr = json_decode($accessTokenJSON, true);
$accessTokenArr['timestamp'] = time();
$write = $this->createFile(json_encode($accessTokenArr));
if (isset($accessTokenArr['access_token']) && $write) {
return $accessTokenArr['access_token'];
} else {
die($accessTokenJSON);
}
}

/**
* 读取accessToken
* @return string
*/
public function getAccessToken()
{
$currentTimestamp = time(); // 当前时间戳
$accessTokenJSON = file_get_contents(__DIR__ . '/../wechat/params/token.json');
$accessTokenArr = json_decode($accessTokenJSON, true);
$timestamp = $currentTimestamp - $accessTokenArr['timestamp'];
if ($timestamp < 7200) {// token 有效期2个小时
return $accessTokenArr['access_token'];
} else {//请求接口
return $this->createAccessToken();
}
}

/**
* 创建临时文件
* @param $content
* @return boolean 创建文件是否成功
*/
public function createFile($content)
{
$filename = __DIR__ . '/../wechat/params/token.json';
$file = fopen($filename, "w") or die("Unable to open file!");
$txt = $content;
$count = strlen($content);
if ($count === fwrite($file, $txt)) {// 根据写入内容长度判断文件是否写入,
fclose($file);
return true;
} else {
fclose($file);
return false;
}
}
}

2.JS-SDK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class Jssdk
{
private $appId;
private $appSecret;

public function __construct()
{
$this->appId = Yii::$app->params['WeChat']['AppID'];
$this->appSecret = Yii::$app->params['WeChat']['AppSecret'];
}

public function getSignPackage()
{
$jsapiTicket = $this->getJsApiTicket();

// 注意 URL 一定要动态获取,不能 hardcode.
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";

$timestamp = time();
$nonceStr = $this->createNonceStr();

// 这里参数的顺序要按照 key 值 ASCII 码升序排序
$string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr&timestamp=$timestamp&url=$url";

$signature = sha1($string);

$signPackage = array(
"appId" => $this->appId,
"nonceStr" => $nonceStr,
"timestamp" => $timestamp,
"url" => $url,
"signature" => $signature,
"rawString" => $string
);
return $signPackage;
}

private function createNonceStr($length = 16)
{
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}

private function getJsApiTicket()
{
// jsapi_ticket 应该全局存储与更新,以下代码以写入到文件中做示例
$data = json_decode($this->get_ticket_file(), true);
if ($data['expire_time'] < time()) {
$tokenClass = new Token();
$accessToken = $tokenClass->getAccessToken();
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
$resJson = file_get_contents($url);
$res = json_decode($resJson, true);
if (isset($res['ticket'])) {
$ticket = $res['ticket'];
$data['expire_time'] = time() + 7200;
$data['jsapi_ticket'] = $ticket;
$this->set_php_file(json_encode($data));
} else {
die($resJson);
}
} else {
$ticket = $data['jsapi_ticket'];
}

return $ticket;
}


private function get_ticket_file()
{
return file_get_contents(__DIR__ . "/../wechat/params/jsapi_ticket.json");
}

private function set_php_file($content)
{
$filename = __DIR__ . "/../wechat/params/jsapi_ticket.json";
$fp = fopen($filename, "w");
fwrite($fp, $content);
fclose($fp);
}
}

通过getSignPackage()获取配置信息后就可以在网页部分调用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script>
wx.config({
debug: true,
appId: '<?php echo $signPackage["appId"];?>',
timestamp: <?php echo $signPackage["timestamp"];?>,
nonceStr: '<?php echo $signPackage["nonceStr"];?>',
signature: '<?php echo $signPackage["signature"];?>',
jsApiList: [
// 所有要调用的 API 都要加到这个列表中
]
});
wx.ready(function () {
// 在这里调用 API
'checkJsApi',
'onMenuShareTimeline',
'onMenuShareAppMessage',
//......
});
</script>

3.网页授权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

<?php
public function oauth()
{
if (!isset($_SESSION['user'])) {
if (isset($_GET['code'])) {//获取到授权code
$code = $_GET['code'];
$appid = Yii::$app->params['WeChat']['AppID'];
$secret = Yii::$app->params['WeChat']['AppSecret'];
//获取access_token和openid
$url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=$appid&secret=$secret&code=$code&grant_type=authorization_code";
$getTokenJson = file_get_contents($url);
$getTokenArray = json_decode($getTokenJson, true);
$openId = $getTokenArray['openid'];
$accessToken = $getTokenArray['access_token'];
//查询本地数据库判断用户是否存在
$user = UserService::getUserByOpenId($openId);

//请求微信接口获取用户信息
$url = "https://api.weixin.qq.com/sns/userinfo?access_token=$accessToken&openid=$openId&lang=zh_CN";
$getTokenUser = file_get_contents($url);
$getTokenUser = json_decode($getTokenUser, true);
//包括openid,nickname,sex,province,city,country,headimgurl
$data = array(
"subscribe" => 1,
"name" => $getTokenUser["nickname"],
"openid" => $getTokenUser["openid"],
"headimgurl" => $getTokenUser["headimgurl"],
"sex" => $getTokenUser["sex"],
);
//用户存在则更新最新信息
if ($user) {
$user_id = $user['id'];
UserService::udateUser($data, $user_id);
} else {
$user_id = UserService::addUser($data);
}
$_SESSION['user'] = UserService::getUserByUserId($user_id);

} else {//请求oauth2授权
$callback = urlencode("http://" . $_SERVER['HTTP_HOST'] . strip_tags($_SERVER['REQUEST_URI']));
$state = '';
$url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=$appid&redirect_uri=$callback&response_type=code&scope=snsapi_userinfo&state=$state#wechat_redirect";
header("Location: $url");
exit;
}
}

return $_SESSION['user'];
}

4.服务器配置
这里可以用到微信公众平台PHP-SDK中的wechat.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
class Weixin
{
public static $weObj;
public static $revData;
public static $revFrom;

public function __construct()
{
$config = Yii::$app->params['WeChat'];
$options = array(
'token' => $config["Token"], //填写你设定的key
'encodingaeskey' => $config["EncodingAESKey"], //填写加密用的EncodingAESKey
'appid' => $config["AppID"], //填写高级调用功能的app id
'appsecret' => $config["AppSecret"], //填写高级调用功能的密钥
);
//wechat.class.php库
self::$weObj = new Wechat($options);
}

public function actionIndex()
{
if ($_GET) {
self::$weObj->valid();
} else {
if (!self::$weObj->valid(true)) {
die('no access!!!');
}
}
$type = self::$weObj->getRev()->getRevType();
self::$revData = self::$weObj->getRevData();
self::$revFrom = self::$weObj->getRevFrom();
$this->check($type);
}


public function check($type)
{
switch ($type) {
case Wechat::MSGTYPE_TEXT:
//text
$this->checkKeywords(self::$weObj->getRev()->getRevContent());
break;
case Wechat::MSGTYPE_EVENT:
//event
$this->checkEvents(self::$revData['Event']);
break;
case Wechat::MSGTYPE_IMAGE:
//image
self::$weObj->text('本系统暂不支持图片信息!')->reply();
break;
default:
self::$weObj->text('本系统暂时无法识别您的指令!')->reply();
}
}

//检测事件(只列举了3种)
public function checkEvents($event)
{
$openid = self::$revData['FromUserName'];
switch ($event) {
case 'subscribe':
$this->checkKeyWords('subscribe');
$this->checkWechatUser($openid);
break;
case 'unsubscribe':
$this->updateUser($openid);
break;
case 'CLICK':
$this->checkKeyWords(self::$revData['EventKey']);
break;
}
}

public function checkWechatUser($openId)
{
$model = new User();
$user = $model->findOne(['openid' => $openId]);
$userInfo = self::$weObj->getUserInfo($openId);
if ($user) {
$model->updateAll(array("subscribe" => 1, "username" => $userInfo["nickname"]), array("id" => $user["id"]));
} else {
$data = array(
"subscribe" => 1,
"username" => $userInfo["nickname"],
"openid" => $userInfo["openid"],
);
if ($model->load([$model->formName() => $data]) && $model->validate()) {
$model->save();
}
}
}

public function updateUser($openId)
{
$model = new User();
$user = $model->findOne(['openid' => $openId]);
if ($user) {
$model->updateAll(array("subscribe" => 0), array("id" => $user["id"]));
}
}

//检测关键字自动回复
public function checkKeyWords($key)
{
$key = $key ? $key : self::$weObj->getRev()->getRevContent();
$replay = WxMessage::getReply($key);
if ($replay) {
//图文消息
if ($replay[0]["type"] == "news") {
$newsArr = array();
foreach ($replay as $value) {
$newsArr [] = array(
'Title' => $value["title"],
'Description' => $value["description"],
'PicUrl' => $value["image"],
'Url' => $value["url"]
);
}
self::$weObj->news($newsArr)->reply();
} else {
//文字信息
self::$weObj->text($replay[0]["title"])->reply();
}
} else {
self::$weObj->text("亲,我们的系统暂时无法识别您的指令哦")->reply();
}
}

//生成微信菜单
public function actionMenu()
{
$menu = WxMenu::getTopMenu();

$newmenu["button"] = array();
for ($i = 0; $i < count($menu); $i++) {
if ($menu[$i]["type"] == "view") {
$sub = WxMenu::getSubMenu($menu[$i]["id"]);
if ($sub) {
$sub_button = array();

for ($j = 0; $j < count($sub); $j++) {
if ($sub[$j]["type"] == "view") {
array_push($sub_button, array('type' => 'view', 'name' => $sub[$j]["name"], 'url' => $sub[$j]["url"]));
} else {
array_push($sub_button, array('type' => 'click', 'name' => $sub[$j]["name"], 'key' => $sub[$j]["key"]));
}
}
array_push($newmenu["button"], array('name' => $menu[$i]["name"], 'sub_button' => $sub_button));
} else {
array_push($newmenu["button"], array('type' => 'view', 'name' => $menu[$i]["name"], 'url' => $menu[$i]["url"]));
}
} else {
$sub = WxMenu::getSubMenu($menu[$i]["id"]);
if ($sub) {
$sub_button = array();

for ($j = 0; $j < count($sub); $j++) {
if ($sub[$j]["type"] == "view") {
array_push($sub_button, array('type' => 'view', 'name' => $sub[$j]["name"], 'url' => $sub[$j]["url"]));
} else {
array_push($sub_button, array('type' => 'click', 'name' => $sub[$j]["name"], 'key' => $sub[$j]["key"]));
}
}
array_push($newmenu["button"], array('name' => $menu[$i]["name"], 'sub_button' => $sub_button));
} else {
array_push($newmenu["button"], array('type' => 'click', 'name' => $menu[$i]["name"], 'key' => $menu[$i]["key"]));
}
}
}

$json = self::$weObj->createMenu($newmenu);
$json = json_decode($json, true);

if ($json["errcode"] === 0) {
$result = ["code" => 0, "message" => 'success', "object" => null];
} else {
$result = ["code" => 1, "message" => 'fail', "object" => $json];
}
return json_encode($result);

}

}

这里可以用的2张表
菜单表

id type name key url pid order
1 view 菜单一 http://m.baidu.com 0 1
2 view 菜单二 key http://m.baidu.com 0 2
3 click 菜单三 0 3
4 view 子菜单 http://m.baidu.com 3 1

关键字回复表

id type title description url image key
1 news 菜单三的回复 描述 http://m.baidu.com 1.jpg key
2 news 欢迎关注 描述 http://m.baidu.com 2.jpg subscribe
3 text 测试 http://m.baidu.com 测试

可以看到菜单3是click按钮,点击菜单3(key)会回复一个图文消息
关注事件也会回复一个图文消息
给公众号发测试会回复一条文字消息

大致就这些了,如有错误欢迎指正

具体可参考微信开发者文档