这份模板基于微信官方的接口,使用了 cURL 进行 HTTP 请求,是 PHP 开发中最常用和稳定的方式。

(图片来源网络,侵删)
第一步:准备工作
在开始写代码之前,请确保你已经完成以下准备工作:
- 拥有一个已认证的微信公众号:个人订阅号没有自定义菜单权限。
- 获取开发者凭证:
- 登录微信公众平台:https://mp.weixin.qq.com/
- 进入 “设置与开发” -> “基本配置”,找到你的 AppID 和 AppSecret。
- 获取服务器配置信息:
- 在 “基本配置” 页面,找到并记录你的 服务器地址(URL)、Token 和 EncodingAESKey。
- 注意:自定义菜单的创建和获取接口,对服务器地址的 URL Scheme 没有强制要求(即不一定是
https://yourdomain.com/wechat),但接收菜单点击事件消息的接口必须是完整配置好的。
第二步:理解微信自定义菜单的数据结构
微信自定义菜单是一个 JSON 对象,我们先来看一个典型的“一级菜单 + 二级菜单”的例子:
{
"button": [
{
"name": "官网",
"type": "view",
"url": "https://www.your-website.com"
},
{
"name": "会员中心",
"type": "click",
"key": "USER_MEMBER_CENTER"
},
{
"name": "更多",
"sub_button": [
{
"name": "扫码带提示",
"type": "scancode_push",
"key": "rselfmenu_0_0"
},
{
"name": "拍照发图",
"type": "pic_sysphoto",
"key": "rselfmenu_0_1"
},
{
"name": "发送位置",
"type": "location_select",
"key": "rselfmenu_0_2"
}
]
}
]
}
关键字段说明:
button: 一个数组,代表菜单列表。name: 按钮的名称(中文,必须在16个字符以内)。type: 按钮类型,决定url或key是否必需。click: 点击推事件,用户点击按钮后,微信会推送click事件给开发者,必须有key。view: 跳转URL,用户点击按钮后,客户端将会打开url链接,必须有url。scancode_push: 扫码推事件,用户点击按钮后,客户端将调起扫一扫工具,必须有key。pic_sysphoto: 弹出系统拍照发图,用户点击按钮后,客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,必须有key。pic_photo_or_album: 弹出拍照或者相册发图,用户点击按钮后,客户端将弹出选择器,用户可以选择拍照或者从手机相册中选图,必须有key。pic_weixin: 弹出微信相册发图器,用户点击按钮后,客户端将调起微信相册,完成选择操作后,将选择的相片发送给开发者,必须有key。location_select: 弹出地理位置选择器,用户点击按钮后,客户端将调起地理位置选择工具,必须有key。
url:type为view时必须,网页链接。key:click、scancode_push等事件类型时必须,与消息接口中的EventKey对应,用于标识按钮。sub_button: 子按钮数组,只有当某个按钮没有type、url或key时,才能包含此字段,从而形成二级菜单。
第三步:PHP 核心代码模板
我们将创建几个核心函数来处理微信 API 的交互:获取 Access Token、创建菜单、查询菜单、删除菜单。

(图片来源网络,侵删)
配置文件 (config.php)
建议将敏感信息放在一个单独的配置文件中。
<?php
// config.php
// 你的微信公众号 AppID
define('WECHAT_APPID', 'your_appid_here');
// 你的微信公众号 AppSecret
define('WECHAT_APPSECRET', 'your_appsecret_here');
// 微信 API 基础 URL
define('WECHAT_API_BASE_URL', 'https://api.weixin.qq.com/cgi-bin/');
微信 API 交互类 (WeChatMenu.php)
这是核心逻辑文件,包含了所有与菜单相关的操作。
<?php
// WeChatMenu.php
require_once 'config.php';
class WeChatMenu
{
/**
* 获取微信 Access Token
* @return string|false 成功返回 token,失败返回 false
*/
public function getAccessToken()
{
$url = WECHAT_API_BASE_URL . "token?grant_type=client_credential&appid=" . WECHAT_APPID . "&secret=" . WECHAT_APPSECRET;
$response = $this->httpRequest($url);
if ($response) {
$data = json_decode($response, true);
if (isset($data['access_token'])) {
return $data['access_token'];
}
}
return false;
}
/**
* 创建自定义菜单
* @param array $menuData 菜单数据数组
* @return bool|string 成功返回 "ok",失败返回错误信息
*/
public function createMenu(array $menuData)
{
$accessToken = $this->getAccessToken();
if (!$accessToken) {
return "获取 Access Token 失败";
}
$url = WECHAT_API_BASE_URL . "menu/create?access_token=" . $accessToken;
// 将数组转换为 JSON 字符串,JSON_UNESCAPED_UNICODE 确保中文不被转义
$jsonData = json_encode($menuData, JSON_UNESCAPED_UNICODE);
$response = $this->httpRequest($url, $jsonData, 'POST');
if ($response) {
$result = json_decode($response, true);
if (isset($result['errcode']) && $result['errcode'] == 0) {
return "ok";
} else {
return isset($result['errmsg']) ? $result['errmsg'] : '未知错误';
}
}
return "请求微信API失败";
}
/**
* 查询自定义菜单
* @return array|false 成功返回菜单数组,失败返回 false
*/
public function getMenu()
{
$accessToken = $this->getAccessToken();
if (!$accessToken) {
return false;
}
$url = WECHAT_API_BASE_URL . "menu/get?access_token=" . $accessToken;
$response = $this->httpRequest($url);
if ($response) {
$result = json_decode($response, true);
// 如果菜单存在,返回菜单结构
if (isset($result['menu'])) {
return $result['menu'];
}
}
return false;
}
/**
* 删除自定义菜单
* @return bool|string 成功返回 "ok",失败返回错误信息
*/
public function deleteMenu()
{
$accessToken = $this->getAccessToken();
if (!$accessToken) {
return "获取 Access Token 失败";
}
$url = WECHAT_API_BASE_URL . "menu/delete?access_token=" . $accessToken;
$response = $this->httpRequest($url);
if ($response) {
$result = json_decode($response, true);
if (isset($result['errcode']) && $result['errcode'] == 0) {
return "ok";
} else {
return isset($result['errmsg']) ? $result['errmsg'] : '未知错误';
}
}
return "请求微信API失败";
}
/**
* 通用的 HTTP 请求方法
* @param string $url 请求地址
* @param string $data POST 请求的数据
* @param string $method 请求方法 GET/POST
* @return string|false 响应内容或 false
*/
private function httpRequest($url, $data = null, $method = 'GET')
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过SSL证书检查,开发环境使用,生产环境建议开启
if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($data)
]);
}
$output = curl_exec($ch);
// 检查是否有 cURL 错误
if (curl_errno($ch)) {
error_log('cURL Error: ' . curl_error($ch));
curl_close($ch);
return false;
}
curl_close($ch);
return $output;
}
}
调用示例文件 (manage_menu.php)
这个文件展示了如何使用上面的 WeChatMenu 类来管理你的菜单。
<?php
// manage_menu.php
require_once 'WeChatMenu.php';
$menuManager = new WeChatMenu();
// --- 示例 1: 创建一个新菜单 ---
// 定义你的菜单结构
$newMenu = [
'button' => [
[
'name' => '官网',
'type' => 'view',
'url' => 'https://www.your-website.com'
],
[
'name' => '关于我们',
'type' => 'click',
'key' => 'ABOUT_US'
],
[
'name' => '更多服务',
'sub_button' => [
[
'name' => '扫码关注',
'type' => 'scancode_push',
'key' => 'SCAN_CODE'
],
[
'name' => '联系我们',
'type' => 'click',
'key' => 'CONTACT_US'
],
[
'name' => '优惠活动',
'type' => 'view',
'url' => 'https://www.your-website.com/promo'
]
]
]
]
];
echo "正在尝试创建菜单...\n";
$result = $menuManager->createMenu($newMenu);
if ($result === "ok") {
echo "菜单创建成功!\n";
} else {
echo "菜单创建失败: " . $result . "\n";
}
// --- 示例 2: 查询当前菜单 ---
echo "\n正在查询当前菜单...\n";
$currentMenu = $menuManager->getMenu();
if ($currentMenu) {
echo "当前菜单配置:\n";
print_r($currentMenu);
} else {
echo "查询菜单失败或菜单不存在,\n";
}
// --- 示例 3: 删除菜单 ---
// echo "\n正在尝试删除菜单...\n";
// $deleteResult = $menuManager->deleteMenu();
// if ($deleteResult === "ok") {
// echo "菜单删除成功!\n";
// } else {
// echo "菜单删除失败: " . $deleteResult . "\n";
// }
第四步:如何使用
- 部署文件:将
config.php、WeChatMenu.php和manage_menu.php这三个文件上传到你的 PHP 服务器上。 - 修改配置:打开
config.php,将your_appid_here和your_appsecret_here替换成你自己的 AppID 和 AppSecret。 - 执行脚本:
- 你可以通过浏览器访问
http://yourserver.com/manage_menu.php来执行脚本。 - 更推荐的方式是通过命令行执行,这样可以看到更清晰的输出:
php /path/to/your/server/manage_menu.php
- 你可以通过浏览器访问
- 验证结果:
- 创建成功后,用微信扫描公众号的二维码,关注后进入聊天界面,点击底部的 “+” 号,你应该能看到你刚刚创建的菜单了。
- 如果菜单没有出现,请检查:
- Token 配置:确保服务器配置的 Token 和 EncodingAESKey 是正确的,并且能正确处理微信的验证消息。
- AppID/Secret:是否输入正确。
- 菜单数据:JSON 格式是否正确,可以使用在线 JSON 格式化工具检查。
- Access Token:是否已过期(Access Token 有效期为 2 小时,每次创建都重新获取)。
重要注意事项
- Access Token 缓存:
getAccessToken()接口有调用频率限制(2000次/天),且 token 有效期只有 2 小时,在生产环境中,强烈建议你将获取到的 token 缓存起来(例如存到 Redis、Memcached 或文件中),在过期后再重新获取,而不是每次调用都去请求微信服务器。 - 环境:上面的代码在
httpRequest函数中设置了CURLOPT_SSL_VERIFYPEER, false,这在开发阶段很方便,但在生产环境中为了安全,应该开启 SSL 证书验证。 - 错误处理:示例中的错误处理比较简单,实际项目中应该更完善,例如记录日志、重试机制等。
- 按钮限制:自定义菜单按钮总数不能超过 3 个,一级菜单名称不能超过 4 个汉字,二级菜单名称不能超过 16 个汉字。
