本文目录导读:

我来为您详细介绍PHP项目中公众号菜单管理的实现方案。
数据表设计
-- 公众号账号表
CREATE TABLE `wechat_accounts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '公众号名称',
`app_id` varchar(50) NOT NULL COMMENT 'AppID',
`app_secret` varchar(100) NOT NULL COMMENT 'AppSecret',
`token` varchar(100) NOT NULL COMMENT 'Token',
`encoding_aes_key` varchar(100) DEFAULT NULL COMMENT '编码密钥',
`status` tinyint(1) DEFAULT 1 COMMENT '状态 1启用 0禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 菜单表
CREATE TABLE `wechat_menus` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`account_id` int(11) NOT NULL COMMENT '关联公众号ID',
`parent_id` int(11) DEFAULT 0 COMMENT '父级菜单ID',
`name` varchar(40) NOT NULL COMMENT '菜单名称',
`type` enum('click','view','miniprogram','scancode_waitmsg','scancode_push',
'pic_sysphoto','pic_photo_or_album','pic_weixin','location_select')
DEFAULT 'click' COMMENT '菜单类型',
`key_value` varchar(128) DEFAULT NULL COMMENT 'key值(click类型)',
`url` varchar(255) DEFAULT NULL COMMENT '链接(view类型)',
`appid` varchar(50) DEFAULT NULL COMMENT '小程序appid',
`pagepath` varchar(100) DEFAULT NULL COMMENT '小程序页面路径',
`media_id` varchar(100) DEFAULT NULL COMMENT '素材ID',
`sort_order` int(11) DEFAULT 0 COMMENT '排序',
`status` tinyint(1) DEFAULT 1 COMMENT '状态',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `account_id` (`account_id`),
KEY `parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
核心功能实现
菜单管理类
<?php
namespace App\Service\Wechat;
use think\facade\Db;
use think\facade\Cache;
class MenuService
{
protected $wechatService;
public function __construct()
{
$this->wechatService = new WechatService();
}
/**
* 获取菜单列表(树形结构)
*/
public function getMenuTree($accountId)
{
$menus = Db::name('wechat_menus')
->where('account_id', $accountId)
->where('status', 1)
->order('parent_id', 'asc')
->order('sort_order', 'asc')
->select()
->toArray();
return $this->buildTree($menus, 0);
}
/**
* 构建树形结构
*/
private function buildTree($menus, $parentId = 0)
{
$tree = [];
foreach ($menus as $menu) {
if ($menu['parent_id'] == $parentId) {
$children = $this->buildTree($menus, $menu['id']);
if ($children) {
$menu['sub_button'] = $children;
}
$tree[] = $menu;
}
}
return $tree;
}
/**
* 保存菜单
*/
public function saveMenu($accountId, $menuData)
{
// 清空原有菜单
Db::name('wechat_menus')
->where('account_id', $accountId)
->delete();
// 批量插入菜单
return $this->insertMenus($accountId, $menuData, 0);
}
/**
* 递归插入菜单
*/
private function insertMenus($accountId, $menus, $parentId = 0, $sort = 0)
{
foreach ($menus as $menu) {
$insertData = [
'account_id' => $accountId,
'parent_id' => $parentId,
'name' => $menu['name'],
'type' => $menu['type'] ?? 'click',
'key_value' => $menu['key'] ?? '',
'url' => $menu['url'] ?? '',
'appid' => $menu['appid'] ?? '',
'pagepath' => $menu['pagepath'] ?? '',
'media_id' => $menu['media_id'] ?? '',
'sort_order' => $sort++,
'create_time' => date('Y-m-d H:i:s')
];
$id = Db::name('wechat_menus')->insertGetId($insertData);
// 处理子菜单
if (!empty($menu['sub_button'])) {
$this->insertMenus($accountId, $menu['sub_button'], $id);
}
}
}
/**
* 发布菜单到微信服务器
*/
public function publishMenu($accountId)
{
try {
// 获取菜单数据
$menus = $this->getMenuTree($accountId);
if (empty($menus)) {
return ['code' => 0, 'msg' => '菜单数据为空'];
}
// 转换菜单格式
$menuData = $this->formatMenuData($menus);
// 获取微信操作对象
$wechatApp = $this->wechatService->getApp($accountId);
// 发布菜单
$result = $wechatApp->menu->create($menuData);
// 记录发布日志
$this->logMenuPublish($accountId, $result);
return [
'code' => 1,
'msg' => '发布成功',
'data' => $result
];
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '发布失败:' . $e->getMessage()
];
}
}
/**
* 格式化菜单数据(适配微信接口)
*/
private function formatMenuData($menus)
{
$button = [];
foreach ($menus as $menu) {
$item = [
'name' => $menu['name']
];
// 有子菜单
if (!empty($menu['sub_button'])) {
$item['sub_button'] = $this->formatMenuData($menu['sub_button']);
} else {
// 不同类型的菜单配置
switch ($menu['type']) {
case 'click':
$item['type'] = 'click';
$item['key'] = $menu['key_value'] ?: $menu['name'];
break;
case 'view':
$item['type'] = 'view';
$item['url'] = $menu['url'];
break;
case 'miniprogram':
$item['type'] = 'miniprogram';
$item['url'] = $menu['url'];
$item['appid'] = $menu['appid'];
$item['pagepath'] = $menu['pagepath'];
break;
case 'scancode_waitmsg':
case 'scancode_push':
case 'pic_sysphoto':
case 'pic_photo_or_album':
case 'pic_weixin':
case 'location_select':
$item['type'] = $menu['type'];
$item['key'] = $menu['key_value'] ?: $menu['name'];
break;
}
}
$button[] = $item;
}
return ['button' => $button];
}
/**
* 删除菜单
*/
public function deleteMenu($accountId)
{
try {
$wechatApp = $this->wechatService->getApp($accountId);
$result = $wechatApp->menu->delete();
// 删除本地菜单
Db::name('wechat_menus')
->where('account_id', $accountId)
->delete();
return [
'code' => 1,
'msg' => '删除成功'
];
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '删除失败:' . $e->getMessage()
];
}
}
/**
* 获取当前菜单(从微信服务器)
*/
public function getCurrentMenu($accountId)
{
try {
$wechatApp = $this->wechatService->getApp($accountId);
$currentMenu = $wechatApp->menu->current();
return [
'code' => 1,
'data' => $currentMenu
];
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '获取失败:' . $e->getMessage()
];
}
}
/**
* 记录菜单发布日志
*/
private function logMenuPublish($accountId, $result)
{
$logData = [
'account_id' => $accountId,
'result' => json_encode($result),
'create_time' => date('Y-m-d H:i:s')
];
Db::name('wechat_menu_log')->insert($logData);
}
}
控制器实现
<?php
namespace App\Controller\Admin;
use App\Service\Wechat\MenuService;
use think\facade\View;
class WechatMenuController extends BaseController
{
protected $menuService;
public function __construct()
{
parent::__construct();
$this->menuService = new MenuService();
}
/**
* 菜单管理页面
*/
public function index()
{
$accountId = input('account_id', 0);
$menus = $this->menuService->getMenuTree($accountId);
View::assign([
'account_id' => $accountId,
'menus' => json_encode($menus)
]);
return View::fetch('wechat/menu/index');
}
/**
* 保存菜单
*/
public function save()
{
$accountId = input('account_id', 0);
$menuData = input('menu_data/a', []);
if (empty($accountId) || empty($menuData)) {
return json(['code' => 0, 'msg' => '参数错误']);
}
$result = $this->menuService->saveMenu($accountId, $menuData);
return json(['code' => 1, 'msg' => '保存成功']);
}
/**
* 发布菜单
*/
public function publish()
{
$accountId = input('account_id', 0);
if (empty($accountId)) {
return json(['code' => 0, 'msg' => '参数错误']);
}
$result = $this->menuService->publishMenu($accountId);
return json($result);
}
/**
* 删除菜单
*/
public function delete()
{
$accountId = input('account_id', 0);
if (empty($accountId)) {
return json(['code' => 0, 'msg' => '参数错误']);
}
$result = $this->menuService->deleteMenu($accountId);
return json($result);
}
/**
* 获取当前菜单
*/
public function getCurrent()
{
$accountId = input('account_id', 0);
if (empty($accountId)) {
return json(['code' => 0, 'msg' => '参数错误']);
}
$result = $this->menuService->getCurrentMenu($accountId);
return json($result);
}
}
前端页面示例(Vue.js)
<template>
<div class="menu-manager">
<div class="menu-preview">
<div class="phone-frame">
<div class="phone-header">公众号</div>
<div class="phone-content">
<!-- 预览内容 -->
</div>
<div class="menu-bar">
<div
v-for="(menu, index) in menus"
:key="index"
class="menu-item"
:class="{ active: activeMenu === index }"
@click="selectMenu(index)"
>
{{ menu.name }}
<div v-if="menu.sub_button && activeMenu === index" class="sub-menu">
<div
v-for="(sub, subIndex) in menu.sub_button"
:key="subIndex"
class="sub-menu-item"
>
{{ sub.name }}
</div>
</div>
</div>
<div class="add-menu" @click="addMenu">+</div>
</div>
</div>
</div>
<div class="menu-editor">
<el-form :model="currentEditMenu" label-width="80px">
<el-form-item label="菜单名称">
<el-input v-model="currentEditMenu.name" maxlength="4"></el-input>
</el-form-item>
<el-form-item label="菜单类型">
<el-select v-model="currentEditMenu.type" @change="onTypeChange">
<el-option label="点击事件" value="click"></el-option>
<el-option label="跳转网页" value="view"></el-option>
<el-option label="小程序" value="miniprogram"></el-option>
<el-option label="扫码" value="scancode_push"></el-option>
<el-option label="拍照" value="pic_sysphoto"></el-option>
<el-option label="地理位置" value="location_select"></el-option>
</el-select>
</el-form-item>
<!-- 根据类型显示不同配置 -->
<el-form-item v-if="currentEditMenu.type === 'view'" label="链接URL">
<el-input v-model="currentEditMenu.url"></el-input>
</el-form-item>
<el-form-item v-if="currentEditMenu.type === 'click'" label="Key值">
<el-input v-model="currentEditMenu.key"></el-input>
</el-form-item>
<el-form-item v-if="currentEditMenu.type === 'miniprogram'">
<el-input v-model="currentEditMenu.appid" placeholder="小程序appid"></el-input>
<el-input v-model="currentEditMenu.pagepath" placeholder="小程序页面路径"></el-input>
</el-form-item>
</el-form>
<div class="editor-actions">
<el-button type="primary" @click="saveMenu">保存菜单</el-button>
<el-button type="success" @click="publishMenu">发布到微信</el-button>
<el-button type="danger" @click="deleteMenu">删除菜单</el-button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
menus: [],
activeMenu: -1,
currentEditMenu: {
name: '',
type: 'click',
key: '',
url: '',
appid: '',
pagepath: ''
}
}
},
methods: {
addMenu() {
// 限制最多3个一级菜单
if (this.menus.length >= 3) {
this.$message.warning('最多只能有3个一级菜单');
return;
}
this.menus.push({
name: '新建菜单',
sub_button: []
});
},
selectMenu(index) {
this.activeMenu = index;
this.currentEditMenu = { ...this.menus[index] };
},
saveMenu() {
// 保存菜单到本地
this.menus[this.activeMenu] = { ...this.currentEditMenu };
},
async publishMenu() {
const res = await this.$http.post('/admin/wechat_menu/publish', {
account_id: this.accountId
});
if (res.code === 1) {
this.$message.success('发布成功');
} else {
this.$message.error(res.msg);
}
},
async deleteMenu() {
const res = await this.$http.post('/admin/wechat_menu/delete', {
account_id: this.accountId
});
if (res.code === 1) {
this.$message.success('删除成功');
this.menus = [];
}
}
}
}
</script>
微信服务基类
<?php
namespace App\Service\Wechat;
use EasyWeChat\Factory;
use think\facade\Db;
use think\facade\Cache;
class WechatService
{
/**
* 获取微信应用实例
*/
public function getApp($accountId)
{
$account = $this->getAccount($accountId);
if (empty($account)) {
throw new \Exception('公众号不存在');
}
$config = [
'app_id' => $account['app_id'],
'secret' => $account['app_secret'],
'token' => $account['token'],
'aes_key' => $account['encoding_aes_key'],
'log' => [
'default' => 'app',
'channels' => [
'app' => [
'level' => 'debug',
'file' => runtime_path() . '/log/wechat.log',
],
],
],
'http' => [
'max_retries' => 3,
'retry_delay' => 500,
'timeout' => 5.0,
],
];
// 配置缓存
$config['cache'] = function () {
return Cache::store('redis');
};
return Factory::officialAccount($config);
}
/**
* 获取公众号信息
*/
protected function getAccount($accountId)
{
$account = Db::name('wechat_accounts')
->where('id', $accountId)
->where('status', 1)
->find();
return $account;
}
}
菜单限制说明
微信菜单规则
- 一级菜单最多3个
- 每个一级菜单下子菜单最多5个
- 菜单名称长度限制
- 一级菜单:4个汉字(或8个字符)
- 子菜单:8个汉字(或16个字符)
- 不同类型的菜单有不同的配置要求
发布注意事项
// 发布前检查
public function validateMenuBeforePublish($menus)
{
$errors = [];
// 检查一级菜单数量
if (count($menus) > 3) {
$errors[] = '一级菜单不能超过3个';
}
foreach ($menus as $menu) {
// 检查菜单名称长度
if (mb_strlen($menu['name']) > 4) {
$errors[] = '一级菜单"' . $menu['name'] . '"名称过长';
}
// 检查子菜单
if (!empty($menu['sub_button'])) {
if (count($menu['sub_button']) > 5) {
$errors[] = $menu['name'] . '的子菜单不能超过5个';
}
foreach ($menu['sub_button'] as $sub) {
if (mb_strlen($sub['name']) > 8) {
$errors[] = '子菜单"' . $sub['name'] . '"名称过长';
}
}
}
}
return $errors;
}
这个方案提供了完整的公众号菜单管理功能,包括:
- 菜单的CRUD操作
- 本地与微信服务器的同步
- 可视化的编辑界面
- 完整的错误处理