前言
开发前,根据需求,阅读微信官方文档:
1、获取accesstoken:微信开发文档-获取access_token
2、自定义菜单:微信开放文档-自定义菜单
一、需求
通过代码实现微信公众号的自定义菜单,包括删除、新增、同步菜单。
二、思路
1、获取微信公众号access_token
2、根据access_token可以查看公众号菜单
3、通过代码接口实现自定义菜单
三、数据库设计
1、表设计
2、参考数据
一级菜单:热门功能、我的服务、关于我们
二级菜单:企业诉求直通车、自助开锁、一张图V2.0、自助移车
菜单结构:热门功能(企业诉求直通车、自助开锁、一张图V2.0、自助移车)、我的服务、关于我们
▲注意点:type为view时,url不可为空,其余type情况可参考微信开发者文档-自定义菜单。
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
button | 是 | 一级菜单数组,个数应为1~3个 |
sub_button | 否 | 二级菜单数组,个数应为1~5个 |
type | 是 | 菜单的响应动作类型,view表示网页类型,click表示点击类型,miniprogram表示小程序类型 |
name | 是 | 菜单标题,不超过16个字节,子菜单不超过60个字节 |
key | click等点击类型必须 | 菜单KEY值,用于消息接口推送,不超过128字节 |
url | view、miniprogram类型必须 | 网页 链接,用户点击菜单可打开链接,不超过1024字节。 type为miniprogram时,不支持小程序的老版本客户端将打开本url。 |
media_id | media_id类型和view_limited类型必须 | 调用新增永久素材接口返回的合法media_id |
appid | miniprogram类型必须 | 小程序的appid(仅认证公众号可配置) |
pagepath | miniprogram类型必须 | 小程序的页面路径 |
article_id | article_id类型和article_view_limited类型必须 | 发布后获得的合法 article_id |
微信公众号平台开发错误返回码说明
返回码 | 错误码描述 | 说明 |
---|---|---|
40001 | invalid credential | 不合法的调用凭证 |
40002 | invalid grant_type | 不合法的grant_type |
40003 | invalid openid | 不合法的OpenID |
40004 | invalid media type | 不合法的媒体文件类型 |
40007 | invalid media_id | 不合法的media_id |
40008 | invalid message type | 不合法的message_type |
40009 | invalid image size | 不合法的图片大小 |
40010 | invalid voice size | 不合法的语音大小 |
40011 | invalid video size | 不合法的视频大小 |
40012 | invalid thumb size | 不合法的缩略图大小 |
40013 | invalid appid | 不合法的AppID |
40014 | invalid access_token | 不合法的access_token |
40015 | invalid menu type | 不合法的菜单类型 |
40016 | invalid button size | 不合法的菜单按钮个数 |
40017 | invalid button type | 不合法的按钮类型 |
40018 | invalid button name size | 不合法的按钮名称长度 |
40019 | invalid button key size | 不合法的按钮KEY长度 |
40020 | invalid button url size | 不合法的url长度 |
40023 | invalid sub button size | 不合法的子菜单按钮个数 |
40024 | invalid sub button type | 不合法的子菜单类型 |
40025 | invalid sub button name size | 不合法的子菜单按钮名称长度 |
40026 | invalid sub button key size | 不合法的子菜单按钮KEY长度 |
40027 | invalid sub button url size | 不合法的子菜单按钮url长度 |
40029 | invalid code | 不合法或已过期的code |
40030 | invalid refresh_token | 不合法的refresh_token |
40036 | invalid template_id size | 不合法的template_id长度 |
40037 | invalid template_id | 不合法的template_id |
40039 | invalid url size | 不合法的url长度 |
40048 | invalid url domain | 不合法的url域名 |
40054 | invalid sub button url domain | 不合法的子菜单按钮url域名 |
40055 | invalid button url domain | 不合法的菜单按钮url域名 |
40066 | invalid url | 不合法的url |
41001 | access_token missing | 缺失access_token参数 |
41002 | appid missing | 缺失appid参数 |
41003 | refresh_token missing | 缺失refresh_token参数 |
41004 | appsecret missing | 缺失secret参数 |
41005 | media data missing | 缺失二进制媒体文件 |
41006 | media_id missing | 缺失media_id参数 |
41007 | sub_menu data missing | 缺失子菜单数据 |
41008 | missing code | 缺失code参数 |
41009 | missing openid | 缺失openid参数 |
41010 | missing url | 缺失url参数 |
42001 | access_token expired | access_token超时 |
42002 | refresh_token expired | refresh_token超时 |
42003 | code expired | code超时 |
43001 | require GET method | 需要使用GET方法请求 |
43002 | require POST method | 需要使用POST方法请求 |
43003 | require https | 需要使用HTTPS |
43004 | require subscribe | 需要订阅关系 |
44001 | empty media data | 空白的二进制数据 |
44002 | empty post data | 空白的POST数据 |
44003 | empty news data | 空白的news数据 |
44004 | empty content | 空白的内容 |
44005 | empty list size | 空白的列表 |
45001 | media size out of limit | 二进制文件超过限制 |
45002 | content size out of limit | content参数超过限制 |
45003 | title size out of limit | title参数超过限制 |
45004 | description size out of limit | description参数超过限制 |
45005 | url size out of limit | url参数长度超过限制 |
45006 | picurl size out of limit | picurl参数超过限制 |
45007 | playtime out of limit | 播放时间超过限制(语音为60s最大) |
45008 | article size out of limit | article参数超过限制 |
45009 | api freq out of limit | 接口调动频率超过限制 |
45010 | create menu limit | 建立菜单被限制 |
45011 | api limit | 频率限制 |
45012 | template size out of limit | 模板大小超过限制 |
45016 | can't modify sys group | 不能修改默认组 |
45017 | can't set group name too long sys group | 修改组名过长 |
45018 | too many group now, no need to add new | 组数量过多 |
50001 | api unauthorized | 接口未授权 |
四、实现
首先声明,以下代码均在jeecg-boot框架基础之上编写实现。
1、yml配置
#公众号
wechat:
#公众号appid
mpAppId: 公众号appid
#公众号appsecret
mpAppSecret: 公众号appsecret
2、pom导入依赖
我在jeecg-system-biz和jeecg-boot-parent中均导入了以下依赖
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.1.0</version>
</dependency>
3、config类
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.WxMpConfigStorage;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.jeecg.modules.menu.utils.ConstantPropertiesUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WeChatMpConfig {
@Autowired
private ConstantPropertiesUtil constantPropertiesUtil;
@Bean
public WxMpService wxMpService(){
WxMpService wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
return wxMpService;
}
@Bean
public WxMpConfigStorage wxMpConfigStorage(){
WxMpDefaultConfigImpl wxMpConfigStorage = new WxMpDefaultConfigImpl();
wxMpConfigStorage.setAppId(ConstantPropertiesUtil.ACCESS_KEY_ID);
wxMpConfigStorage.setSecret(ConstantPropertiesUtil.ACCESS_KEY_SECRET);
return wxMpConfigStorage;
}
}
4、utils类
(1)ConstantPropertiesUtil
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ConstantPropertiesUtil implements InitializingBean {
@Value("${wechat.mpAppId}")
private String appid;
@Value("${wechat.mpAppSecret}")
private String appsecret;
public static String ACCESS_KEY_ID;
public static String ACCESS_KEY_SECRET;
@Override
public void afterPropertiesSet() throws Exception {
ACCESS_KEY_ID = appid;
ACCESS_KEY_SECRET = appsecret;
}
}
(2)HttpClientUtil
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import java.io.*;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.TimeUnit;
public class HttpClientUtil {
// utf-8字符编码
public static final String CHARSET_UTF_8 = "utf-8";
// HTTP内容类型。
public static final String CONTENT_TYPE_TEXT_HTML = "text/xml";
// HTTP内容类型。相当于form表单的形式,提交数据
public static final String CONTENT_TYPE_FORM_URL = "application/x-www-form-urlencoded";
// HTTP内容类型。相当于form表单的形式,提交数据
public static final String CONTENT_TYPE_JSON_URL = "application/json;charset=utf-8";
// 连接管理器
private static PoolingHttpClientConnectionManager pool;
// 请求配置
private static RequestConfig requestConfig;
static {
try {
//System.out.println("初始化HttpClientTest~~~开始");
SSLContextBuilder builder = new SSLContextBuilder();
builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
builder.build());
// 配置同时支持 HTTP 和 HTPPS
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().register(
"http", PlainConnectionSocketFactory.getSocketFactory()).register(
"https", sslsf).build();
// 初始化连接管理器
pool = new PoolingHttpClientConnectionManager(
socketFactoryRegistry);
// 将最大连接数增加到200,实际项目最好从配置文件中读取这个值
pool.setMaxTotal(200);
// 设置最大路由
pool.setDefaultMaxPerRoute(2);
// 根据默认超时限制初始化requestConfig
int socketTimeout = 10000;
int connectTimeout = 10000;
int connectionRequestTimeout = 10000;
requestConfig = RequestConfig.custom().setConnectionRequestTimeout(
connectionRequestTimeout).setSocketTimeout(socketTimeout).setConnectTimeout(
connectTimeout).build();
//System.out.println("初始化HttpClientTest~~~结束");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
// 设置请求超时时间
requestConfig = RequestConfig.custom().setSocketTimeout(50000).setConnectTimeout(50000)
.setConnectionRequestTimeout(50000).build();
}
/**
* 发送Post请求
*
* @param httpPost
* @return
*/
private static String sendHttpPost(HttpPost httpPost) {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
// 响应内容
String responseContent = null;
try {
// 创建默认的httpClient实例.
httpClient = getHttpClient();
// 配置请求信息
httpPost.setConfig(requestConfig);
// 执行请求
response = httpClient.execute(httpPost);
// 得到响应实例
HttpEntity entity = response.getEntity();
// 可以获得响应头
// Header[] headers = response.getHeaders(HttpHeaders.CONTENT_TYPE);
// for (Header header : headers) {
// System.out.println(header.getName());
// }
// 得到响应类型
// System.out.println(ContentType.getOrDefault(response.getEntity()).getMimeType());
// 判断响应状态
if (response.getStatusLine().getStatusCode() >= 300) {
throw new Exception(
"HTTP Request is not success, Response code is " + response.getStatusLine().getStatusCode());
}
if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) {
responseContent = EntityUtils.toString(entity, CHARSET_UTF_8);
EntityUtils.consume(entity);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 释放资源
if (response != null) {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return responseContent;
}
public static CloseableHttpClient getHttpClient() {
CloseableHttpClient httpClient = HttpClients.custom()
// 设置连接池管理
.setConnectionManager(pool)
// 设置请求配置
.setDefaultRequestConfig(requestConfig)
// 设置重试次数
.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))
.build();
return httpClient;
}
/**
* 发送Get请求
*
* @param httpGet
* @return
*/
private static String sendHttpGet(HttpGet httpGet) {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
// 响应内容
String responseContent = null;
try {
// 创建默认的httpClient实例.
httpClient = getHttpClient();
// 配置请求信息
httpGet.setConfig(requestConfig);
// 执行请求
response = httpClient.execute(httpGet);
// 得到响应实例
HttpEntity entity = response.getEntity();
// 可以获得响应头
// Header[] headers = response.getHeaders(HttpHeaders.CONTENT_TYPE);
// for (Header header : headers) {
// System.out.println(header.getName());
// }
// 得到响应类型
// System.out.println(ContentType.getOrDefault(response.getEntity()).getMimeType());
// 判断响应状态
if (response.getStatusLine().getStatusCode() >= 300) {
throw new Exception(
"HTTP Request is not success, Response code is " + response.getStatusLine().getStatusCode());
}
if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) {
responseContent = EntityUtils.toString(entity, CHARSET_UTF_8);
EntityUtils.consume(entity);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 释放资源
if (response != null) {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return responseContent;
}
/**
* 发送 post请求
*
* @param httpUrl 地址
*/
public static String sendHttpPost(String httpUrl) {
// 创建httpPost
HttpPost httpPost = new HttpPost(httpUrl);
return sendHttpPost(httpPost);
}
/**
* 发送 get请求
*
* @param httpUrl
*/
public static String sendHttpGet(String httpUrl) {
// 创建get请求
HttpGet httpGet = new HttpGet(httpUrl);
httpGet.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
httpGet.addHeader("Accept-Encoding", "gzip, deflate, br");
httpGet.addHeader("Accept-Language", "zh-CN,zh;q=0.9");
httpGet.addHeader("Cache-Control", "max-age=0");
httpGet.addHeader("Connection", "keep-alive");
httpGet.addHeader("Referer", httpUrl);
httpGet.addHeader("Sec-Fetch-Mode", "navigate");
httpGet.addHeader("Sec-Fetch-Site", "same-origin");
httpGet.addHeader("Sec-Fetch-User", "?1");
httpGet.addHeader("Upgrade-Insecure-Requests", "1");
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36");
return sendHttpGet(httpGet);
}
/**
* 发送 post请求(带文件)
*
* @param httpUrl 地址
* @param maps 参数
* @param fileLists 附件
*/
public static String sendHttpPost(String httpUrl, Map<String, String> maps, List<File> fileLists) {
HttpPost httpPost = new HttpPost(httpUrl);// 创建httpPost
MultipartEntityBuilder meBuilder = MultipartEntityBuilder.create();
if (maps != null) {
for (String key : maps.keySet()) {
meBuilder.addPart(key, new StringBody(maps.get(key), ContentType.TEXT_PLAIN));
}
}
if (fileLists != null) {
for (File file : fileLists) {
FileBody fileBody = new FileBody(file);
meBuilder.addPart("files", fileBody);
}
}
HttpEntity reqEntity = meBuilder.build();
httpPost.setEntity(reqEntity);
return sendHttpPost(httpPost);
}
/**
* 发送 post请求
*
* @param httpUrl 地址
* @param params 参数(格式:key1=value1&key2=value2)
*/
public static String sendHttpPost(String httpUrl, String params) {
HttpPost httpPost = new HttpPost(httpUrl);// 创建httpPost
try {
// 设置参数
if (params != null && params.trim().length() > 0) {
StringEntity stringEntity = new StringEntity(params, "UTF-8");
stringEntity.setContentType(CONTENT_TYPE_FORM_URL);
httpPost.setEntity(stringEntity);
}
} catch (Exception e) {
e.printStackTrace();
}
return sendHttpPost(httpPost);
}
/**
* 发送 post请求
*
* @param maps 参数
*/
public static String sendHttpPost(String httpUrl, Map<String, String> maps) {
String parem = convertStringParamter(maps);
return sendHttpPost(httpUrl, parem);
}
/**
* 发送 post请求 发送json数据
*
* @param httpUrl 地址
* @param paramsJson 参数(格式 json)
*/
public static String sendHttpPostJson(String httpUrl, String paramsJson) {
HttpPost httpPost = new HttpPost(httpUrl);// 创建httpPost
try {
// 设置参数
if (paramsJson != null && paramsJson.trim().length() > 0) {
StringEntity stringEntity = new StringEntity(paramsJson, "UTF-8");
stringEntity.setContentType(CONTENT_TYPE_JSON_URL);
httpPost.setEntity(stringEntity);
}
} catch (Exception e) {
e.printStackTrace();
}
return sendHttpPost(httpPost);
}
/**
* 发送 post请求 发送xml数据
*
* @param httpUrl 地址
* @param paramsXml 参数(格式 Xml)
*/
public static String sendHttpPostXml(String httpUrl, String paramsXml) {
HttpPost httpPost = new HttpPost(httpUrl);// 创建httpPost
try {
// 设置参数
if (paramsXml != null && paramsXml.trim().length() > 0) {
StringEntity stringEntity = new StringEntity(paramsXml, "UTF-8");
stringEntity.setContentType(CONTENT_TYPE_TEXT_HTML);
httpPost.setEntity(stringEntity);
}
} catch (Exception e) {
e.printStackTrace();
}
return sendHttpPost(httpPost);
}
/**
* 将map集合的键值对转化成:key1=value1&key2=value2 的形式
*
* @param parameterMap 需要转化的键值对集合
* @return 字符串
*/
public static String convertStringParamter(Map parameterMap) {
StringBuffer parameterBuffer = new StringBuffer();
if (parameterMap != null) {
Iterator iterator = parameterMap.keySet().iterator();
String key = null;
String value = null;
while (iterator.hasNext()) {
key = (String) iterator.next();
if (parameterMap.get(key) != null) {
value = (String) parameterMap.get(key);
} else {
value = "";
}
parameterBuffer.append(key).append("=").append(value);
if (iterator.hasNext()) {
parameterBuffer.append("&");
}
}
}
return parameterBuffer.toString();
}
}
5、entity及封装类
(1)entity
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.models.auth.In;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
@Data
@TableName("wechat_menu")
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class WechatMenu implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
@ApiModelProperty(value = "编号")
private Integer id;
@ApiModelProperty(value = "上级id")
private Integer parentId;
@ApiModelProperty(value = "菜单名称")
private String name;
@ApiModelProperty(value = "类型")
private String type;
@ApiModelProperty(value = "网页链接,用户点击菜单可打开链接")
private String url;
@ApiModelProperty(value = "菜单key值,用于消息接口推送")
private String menuKey;
@ApiModelProperty(value = "排序")
private Integer sort;
@ApiModelProperty(value = "删除标志")
private Integer isDelete;
/**创建时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private java.util.Date createTime;
/**更新时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "更新时间")
private java.util.Date updateTime;
}
(2)vo封装
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class WechatMenuVo implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
@ApiModelProperty(value = "编号")
private String id;
@ApiModelProperty(value = "上级id")
private String parentId;
@ApiModelProperty(value = "菜单名称")
private String name;
@ApiModelProperty(value = "类型")
private String type;
@ApiModelProperty(value = "网页链接,用户点击菜单可打开链接")
private String url;
@ApiModelProperty(value = "菜单key值,用于消息接口推送")
private String menuKey;
@ApiModelProperty(value = "排序")
private Integer sort;
@ApiModelProperty(value = "删除标志")
private Integer isDelete;
/**创建时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private java.util.Date createTime;
/**更新时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "更新时间")
private java.util.Date updateTime;
private List<WechatMenuVo> children;
}
6、mapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.jeecg.modules.menu.entity.WechatMenu;
@Mapper
public interface WechatMenuMapper extends BaseMapper<WechatMenu> {
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.wechatMenu.mapper.WechatMenuMapper">
</mapper>
7、service及serviceImpl类
(1)service
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.menu.entity.WechatMenu;
import org.jeecg.modules.menu.vo.WechatMenuVo;
import java.util.List;
public interface IWechatMenuService extends IService<WechatMenu> {
//获取全部菜单
List<WechatMenuVo> findMenuInfo();
//获取一级菜单
List<WechatMenu> findMenuOneInfo();
//同步公众号菜单
void syncMenu();
//删除公众号菜单
void removeMenu();
}
(2)serviceImpl
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.menu.entity.WechatMenu;
import org.jeecg.modules.menu.mapper.WechatMenuMapper;
import org.jeecg.modules.menu.service.IWechatMenuService;
import org.jeecg.modules.menu.vo.WechatMenuVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class WechatWechatMenuServiceImpl extends ServiceImpl<WechatMenuMapper, WechatMenu> implements IWechatMenuService {
@Autowired
private WxMpService wxMpService;
//获取所有菜单,按照一级和二级菜单封装
@Override
public List<WechatMenuVo> findMenuInfo() {
//1、创建List集合,用于最终数据封装
List<WechatMenuVo> finalMenuList=new ArrayList<>();
//2、查询出所有菜单数据(包含一级和二级)
List<WechatMenu> menuList = baseMapper.selectList(null);
//3、从所有菜单数据中获取所有一级菜单数据(parent_id=0)
List<WechatMenu> oneMenuList = menuList.stream()
.filter(menu -> menu.getParentId() == 0)
.collect(Collectors.toList());
//4、封装一级菜单数据,封装到最终数据list集合
//遍历一级菜单list集合
oneMenuList.forEach(oneMenu->{
//Menu -->MenuVo
WechatMenuVo oneMenuVo = new WechatMenuVo();
BeanUtils.copyProperties(oneMenu,oneMenuVo);
//5、封装二级菜单数据(判断一级菜单id和二级菜单的parent_id是否相同)
//如果相同,把二级菜单数据放到一级菜单里面
List<WechatMenu> twoMenuList = menuList.stream()
.filter(menu -> menu.getParentId().equals(oneMenu.getId()))
.collect(Collectors.toList());
//List<Menu>->List<MenuVo>
List<WechatMenuVo> children=new ArrayList<>();
twoMenuList.forEach(twoMenu -> {
WechatMenuVo twoMenuVo = new WechatMenuVo();
BeanUtils.copyProperties(twoMenu,twoMenuVo);
children.add(twoMenuVo);
});
//把二级菜单数据放到一级菜单里面
oneMenuVo.setChildren(children);
//把oneMenuVo放到最终list集合
finalMenuList.add(oneMenuVo);
});
//返回最终数据
return finalMenuList;
}
//获取所有一级菜单
@Override
public List<WechatMenu> findMenuOneInfo() {
QueryWrapper<WechatMenu> wrapper=new QueryWrapper<>();
wrapper.eq("parent_id",0);
List<WechatMenu> list = baseMapper.selectList(wrapper);
return list;
}
//同步公众号菜单方法
//https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
//https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
@Override
public void syncMenu() {
//获取所有菜单数据
List<WechatMenuVo> menuVoList = this.findMenuInfo();
//封装button里面的结构,数组格式
JSONArray buttonList=new JSONArray();
menuVoList.forEach(oneMenuVo -> {
//json对象 一级菜单
JSONObject one=new JSONObject();
one.put("name",oneMenuVo.getName());
one.put("type",oneMenuVo.getType());
one.put("key",oneMenuVo.getMenuKey());
one.put("url",oneMenuVo.getUrl());
//json数组 二级菜单
JSONArray subButton=new JSONArray();
oneMenuVo.getChildren().forEach(twoMenuVo->{
JSONObject view = new JSONObject();
view.put("type", twoMenuVo.getType());
if(twoMenuVo.getType().equals("view")) {
view.put("name", twoMenuVo.getName());
view.put("url", twoMenuVo.getUrl());
} else {
view.put("name", twoMenuVo.getName());
view.put("key", twoMenuVo.getMenuKey());
}
subButton.add(view);
});
one.put("sub_button",subButton);
buttonList.add(one);
});
//封装最外层的button部分
JSONObject button=new JSONObject();
button.put("button",buttonList);
try {
String menuId =
this.wxMpService.getMenuService().menuCreate(button.toJSONString());
log.info("menuId:{}",menuId);
} catch (WxErrorException e) {
e.printStackTrace();
Result.error(20001,"公众号菜单同步失败");
}
}
//公众号菜单删除
@Override
public void removeMenu() {
try {
wxMpService.getMenuService().menuDelete();
} catch (WxErrorException e) {
e.printStackTrace();
Result.error(20001,"公众号菜单删除失败");
}
}
}
8、controller类
import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.ApiOperation;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.menu.entity.WechatMenu;
import org.jeecg.modules.menu.service.IWechatMenuService;
import org.jeecg.modules.menu.utils.ConstantPropertiesUtil;
import org.jeecg.modules.menu.utils.HttpClientUtil;
import org.jeecg.modules.menu.vo.WechatMenuVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/admin/wechat/menu")
public class WechatMenuController {
@Autowired
private IWechatMenuService menuService;
//公众号菜单删除
@DeleteMapping("/removeMenu")
public Result<?> removeMenu(){
menuService.removeMenu();
return Result.ok(null);
}
//同步菜单方法
@GetMapping("/syncMenu")
public Result<?> createMenu(){
menuService.syncMenu();
WechatMenu wechatMenu = new WechatMenu();
return Result.OK(wechatMenu.getName());
}
//获取access_token
@GetMapping("/getAccessToken")
public Result<?> getAccessToken(){
//拼接请求地址
StringBuffer buffer = new StringBuffer();
buffer.append("https://api.weixin.qq.com/cgi-bin/token");
buffer.append("?grant_type=client_credential");
buffer.append("&appid=%s");
buffer.append("&secret=%s");
//设置路径中的参数
String url = String.format(buffer.toString(),
ConstantPropertiesUtil.ACCESS_KEY_ID,
ConstantPropertiesUtil.ACCESS_KEY_SECRET);
try {
//发送http请求
String tokenString = HttpClientUtil.sendHttpGet(url);
//获取access_token
JSONObject jsonObject = JSONObject.parseObject(tokenString);
String access_token = jsonObject.getString("access_token");
//返回
return Result.OK(access_token);
} catch (Exception e) {
e.printStackTrace();
return Result.error(20001,"获取access_token失败");
//throw new GgktException(20001,"获取access_token失败");
}
}
//获取所有菜单,按照一级和二级菜单封装
@GetMapping("/findMenuInfo")
public Result<?> findMenuInfo(){
List<WechatMenuVo> list=menuService.findMenuInfo();
return Result.ok(list);
}
//获取所有一级菜单
@GetMapping("/findOneMenuInfo")
public Result<?> findOneMenuInfo(){
List<WechatMenu> list=menuService.findMenuOneInfo();
return Result.ok(list);
}
@ApiOperation(value = "获取")
@GetMapping("/get/{id}")
public Result<?> get(@PathVariable Long id) {
WechatMenu wechatMenu = menuService.getById(id);
return Result.ok(wechatMenu);
}
@ApiOperation(value = "新增")
@PostMapping("/save")
public Result<?> save(@RequestBody WechatMenu wechatMenu) {
menuService.save(wechatMenu);
return Result.ok(null);
}
@ApiOperation(value = "修改")
@PutMapping("/update")
public Result<?> updateById(@RequestBody WechatMenu wechatMenu) {
menuService.updateById(wechatMenu);
return Result.OK(wechatMenu);
}
@ApiOperation(value = "删除")
@DeleteMapping("/remove/{id}")
public Result<?> remove(@PathVariable Long id) {
menuService.removeById(id);
return Result.ok(null);
}
@ApiOperation(value = "根据id列表删除")
@DeleteMapping("/batchRemove")
public Result<?> batchRemove(@RequestBody List<Long> idList) {
menuService.removeByIds(idList);
return Result.ok(null);
}
}
总结
1、微信公众号对自定义菜单完成创建、修改等操作的学习过程中参考了以下链接,使用postman测试均无误,在此表示感谢!