本文目录导读:

我来为您详细介绍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操作
- 本地与微信服务器的同步
- 可视化的编辑界面
- 完整的错误处理
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播