最近做了一个公众号菜单管理,需求是后台添加菜单后公众号同步更新,菜单分为一级菜单,二级菜单,二级菜单挂在一级菜单上。效果图如下
菜单sql
CREATE TABLE `wechat_client_bottom_menu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`company_no` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '油企名称',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称',
`menu_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单编码',
`func_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '类型',
`menu_level` int NOT NULL COMMENT '菜单等级',
`order_num` int DEFAULT NULL COMMENT '排序',
`status` int DEFAULT '0' COMMENT '状态 0未发布 1发布',
`created_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
`updated_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` int DEFAULT '0' COMMENT '删除标识符0正常 1删除',
`parent_id` bigint DEFAULT NULL COMMENT '父菜单id',
`parent_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '菜单路径',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='微信公众号底部菜单';
微信公众号菜单对象
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.knight.common.core.annotation.AddGroup;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 微信公众号底部菜单
* </p>
*
* @author ws
* @since 2022-02-28
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="WechatClientBottomMenu对象", description="微信公众号底部菜单")
public class WechatClientBottomMenu implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "油企名称")
@TableField(exist = false)
private String companyName;
@ApiModelProperty(value = "上级菜单名称")
@TableField(exist = false)
private String parentName;
@ApiModelProperty(value = "油企编码")
private String companyNo;
@ApiModelProperty(value = "菜单名称")
@NotBlank(groups = {AddGroup.class}, message = "菜单名称必填")
private String name;
@ApiModelProperty(value = "菜单编码")
@NotBlank(groups = {AddGroup.class}, message = "菜单编码必填")
private String menuValue;
@ApiModelProperty(value = "类型 text、 view")
@NotBlank(groups = {AddGroup.class}, message = "类型 text、 view必填")
private String funcType;
@ApiModelProperty(value = "菜单等级")
@NotBlank(groups = {AddGroup.class}, message = "菜单等级必填")
private Integer menuLevel;
@ApiModelProperty(value = "排序")
@NotBlank(groups = {AddGroup.class}, message = "排序必填")
private Integer orderNum;
@ApiModelProperty("菜单全路径")
private String parentPath;
@ApiModelProperty(value = "状态 0未发布 1发布")
private Integer status;
@ApiModelProperty(value = "父菜单Id")
private Long parentId;
@ApiModelProperty(value = "创建人")
private String createdBy;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createdTime;
@ApiModelProperty(value = "更新人")
private String updatedBy;
@ApiModelProperty(value = "更新时间")
private LocalDateTime updatedTime;
@ApiModelProperty(value = "删除标识符0正常 1删除")
private Integer deleted;
}
创建按钮对象,这里只列举了view类型所需的button,其他类型稍微修改一下就可以了
@Data
public class BasicButton {
private String name;
private String url;
}
@Data
public class ComplexButtons extends BasicButton {
private ViewButton[] sub_button;
}
@Data
public class ViewButton extends BasicButton {
private String type;
private String name;
private String url;
}
@Data
public class MainButton {
private String name;
private String type;
private String key;
private BasicButton[] sub_button;
}
创建树形菜单对象,用于处理层级关系
import java.util.List;
/**
* 树节点父类,所有需要使用{@linkplain BaseTreeHelper}工具类形成树形结构等操作的节点都需要实现该接口
*
* @param <T> 节点id类型
* @author huangke
*/
public interface TreeNode<T, E> {
/**
* 获取节点id
*
* @return 树节点id
*/
T getId();
/**
* 获取该节点的父节点id
*
* @return 父节点id
*/
T getPid();
/**
* 是否是根节点
*
* @return true:根节点
*/
Boolean root();
/**
* 设置节点的子节点列表
*
* @param children 子节点
*/
void setChildren(List<E> children);
/**
* 获取所有子节点
*
* @return 子节点列表
*/
List<E> getChildren();
}
import com.knight.common.core.utils.tree.TreeNode;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ButtomTreeVo implements TreeNode<Long, ButtomTreeVo> {
private static final long serialVersionUID = 1L;
/**
* id
*/
@ApiModelProperty("菜单id")
private Long id;
@ApiModelProperty(value = "类型 view ")
private String funcType;
/**
* 菜单名称
*/
@ApiModelProperty("菜单名称")
private String name;
/**
* 菜单值(前端路由)
*/
@ApiModelProperty("菜单值")
private String menuValue;
/**
* 上级菜单id
*/
@ApiModelProperty("上级菜单")
private Long pid;
/**
* 菜单全路径(用/隔开)
*/
@ApiModelProperty("菜单全路径")
private String parentPath;
/**
* 排序
*/
@ApiModelProperty("排序")
private Integer orderNum;
@ApiModelProperty("子菜单")
private List<ButtomTreeVo> children;
@Override
@ApiModelProperty("父节点")
public Boolean root() {
return pid == null;
}
}
树形菜单工具类
import cn.hutool.core.collection.CollUtil;
import org.apache.commons.compress.utils.Lists;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author ws
* @date 2021/8/29
*/
public class BaseTreeHelper {
/**
* @param <T> 节点类型
* @param <E> 节点id的类型
* @return 树形结构列表
* @author huangke
* @date 2021/2/1
* 根据所有树节点列表,生成含有所有树形结构的列表
**/
public static <T extends TreeNode<E, T>, E> List<T> generateTrees(List<T> data) {
if (CollUtil.isEmpty(data)) {
return Lists.newArrayList();
}
//将集合中所有数据按照父Id进行分组,放入Map中,Map<parntId, List<T>>
Map<String, List<T>> groupByParentIdMap = data.stream().collect(Collectors.groupingBy(item -> item.root() ? "" : Objects.toString(item.getPid())));
//将集合中所有数据以数据Id为key,放入Map中,Map<id,T>
Map<String, T> dataMap = data.stream().collect(Collectors.toMap(item -> item.getId().toString(), t -> t));
List<T> resp = Lists.newArrayList();
//遍历数据,将子节点放入对应父节点Children属性中
groupByParentIdMap.forEach((parentId, values) -> {
if (dataMap.containsKey(parentId)) {
List<T> child = dataMap.get(parentId).getChildren();
if (CollUtil.isEmpty(child)) {
child = Lists.newArrayList();
}
child.addAll(values);
dataMap.get(parentId).setChildren(child);
} else {
resp.addAll(values);
}
});
return resp;
}
/**
* @param parent 父节点
* @param <T> 实际节点类型
* @return 叶子节点
* @author huangke
* @date 2021/2/1
* 获取指定树节点下的所有叶子节点
**/
public static <T extends TreeNode<E, T>, E> List<T> getLeafs(T parent) {
List<T> leafs = new ArrayList<>();
fillLeaf(parent, leafs);
return leafs;
}
/**
* @param parent 父节点
* @param leafs 叶子节点列表
* @param <T> 实际节点类型
* @author huangke
* @date 2021/2/1
* 将parent的所有叶子节点填充至leafs列表中
**/
private static <T extends TreeNode<E, T>, E> void fillLeaf(T parent, List<T> leafs) {
List<T> children = parent.getChildren();
// 如果节点没有子节点则说明为叶子节点
if (CollUtil.isEmpty(children)) {
leafs.add(parent);
return;
}
// 递归调用子节点,查找叶子节点
for (T child : children) {
fillLeaf(child, leafs);
}
}
}
创建公众号菜单
//1.菜单创建(POST) 限100(次/天)
public static final String CREATE_MENU_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
@ApiOperation("createMenu:创建公众号底部菜单")
@PostMapping("createMenu")
public Result createMenu() throws Exception {
//查询所有菜单
List<WechatClientBottomMenu> menus = baseService.list(new LambdaQueryWrapper<WechatClientBottomMenu>()
.eq(WechatClientBottomMenu::getDeleted, 0)
.eq(WechatClientBottomMenu::getCompanyNo, SecurityUtils.getCompanyNo())
.orderByAsc(WechatClientBottomMenu::getOrderNum));
List<ButtomTreeVo> wechatMenus = new ArrayList<>();
menus.forEach(menu -> {
ButtomTreeVo mm = ButtomTreeVo.builder().build();
BeanUtil.copyProperties(menu, mm);
mm.setName(menu.getName());
mm.setPid(menu.getParentId());
wechatMenus.add(mm);
});
//生成树形菜单,子节点在children里面
List<ButtomTreeVo> list = BaseTreeHelper.generateTrees(wechatMenus);
List<Object> objects = new ArrayList<>();
for (ButtomTreeVo vo : list) {
//如果有子节点进行处理
if (vo.getChildren() != null) {
ComplexButtons mainBtn1 = new ComplexButtons();
mainBtn1.setName(vo.getName());
List<ViewButton> buttomTreeVos = new ArrayList<>();
for (ButtomTreeVo vv : vo.getChildren()) {
ViewButton viewButton = new ViewButton();
viewButton.setName(vv.getName());
viewButton.setType(vv.getFuncType());
viewButton.setUrl(vv.getParentPath());
buttomTreeVos.add(viewButton);
}
//放入数组中
ViewButton[] array = new ViewButton[buttomTreeVos.size()];
for (int i = 0; i < buttomTreeVos.size(); i++) {
array[i] = buttomTreeVos.get(i);
}
mainBtn1.setSub_button(array);
objects.add(mainBtn1);
} else {
//没有子节点的处理方式
MainButton mainBtn2 = new MainButton();
mainBtn2.setKey(vo.getParentPath());
mainBtn2.setType(vo.getFuncType());
mainBtn2.setName(vo.getName());
objects.add(mainBtn2);
}
}
Map map = new HashMap();
map.put("button", objects);
//生成的map转字符串json发送微信请求
String sb = createMenu(JSON.toJSONString(map),
SecurityUtils.getCompanyNo());
Map mm = JSON.parseObject(sb);
Boolean cc = false;
if (mm.get("errcode").equals(0)) {
cc = true;
}
return cc ? Result.ok(sb) : Result.fail(sb);
}
/**
* @desc :创建菜单
*/
public String createMenu(String menuJson, String companyNo) throws Exception {
//公众号秘钥做了管理的,用companyNo去查询公众号秘钥和appid
WechatClient wechatClient = weChatPublicNoAccountInfoService.getOne(new LambdaQueryWrapper<WechatClient>()
.eq(WechatClient::getCompanyNo, companyNo));
if (wechatClient == null) {
return null;
}
//通过appid秘钥获取accesstoken
String accessToken = wxCommonService.getAccessToken(wechatClient.getAppid(),
wechatClient.getAppSecret());
//2.拼装创建菜单的url
String url = CREATE_MENU_URL.replace("ACCESS_TOKEN", accessToken);
StringBuilder requestUrl = new StringBuilder(url);
//发送pos请求
String result = httpUtils.postRequestJson(requestUrl.toString(), menuJson);
return result;
}
效果如下