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

php 微信自定义菜单 模板
(图片来源网络,侵删)

第一步:准备工作

在开始写代码之前,请确保你已经完成以下准备工作:

  1. 拥有一个已认证的微信公众号:个人订阅号没有自定义菜单权限。
  2. 获取开发者凭证
    • 登录微信公众平台:https://mp.weixin.qq.com/
    • 进入 “设置与开发” -> “基本配置”,找到你的 AppIDAppSecret
  3. 获取服务器配置信息
    • 在 “基本配置” 页面,找到并记录你的 服务器地址(URL)TokenEncodingAESKey
    • 注意:自定义菜单的创建和获取接口,对服务器地址的 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: 按钮类型,决定 urlkey 是否必需。
    • 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: typeview 时必须,网页链接。
  • key: clickscancode_push 等事件类型时必须,与消息接口中的 EventKey 对应,用于标识按钮。
  • sub_button: 子按钮数组,只有当某个按钮没有 typeurlkey 时,才能包含此字段,从而形成二级菜单。

第三步:PHP 核心代码模板

我们将创建几个核心函数来处理微信 API 的交互:获取 Access Token、创建菜单、查询菜单、删除菜单。

php 微信自定义菜单 模板
(图片来源网络,侵删)

配置文件 (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";
// }

第四步:如何使用

  1. 部署文件:将 config.phpWeChatMenu.phpmanage_menu.php 这三个文件上传到你的 PHP 服务器上。
  2. 修改配置:打开 config.php,将 your_appid_hereyour_appsecret_here 替换成你自己的 AppID 和 AppSecret。
  3. 执行脚本
    • 你可以通过浏览器访问 http://yourserver.com/manage_menu.php 来执行脚本。
    • 更推荐的方式是通过命令行执行,这样可以看到更清晰的输出:
      php /path/to/your/server/manage_menu.php
  4. 验证结果
    • 创建成功后,用微信扫描公众号的二维码,关注后进入聊天界面,点击底部的 “+” 号,你应该能看到你刚刚创建的菜单了。
    • 如果菜单没有出现,请检查:
      • 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 个汉字。