1、配置服务器配置信息
2、没有公众号的可以申请微信测试账号
申请地址:
https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
3、校验token 监听事件
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import io.swagger.annotations.Api;
import lombok.AllArgsConstructor;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@AllArgsConstructor
@RequestMapping("/weixin/api")
@Api(value = "测试", tags = "测试")
@ApiSort(1)
public class VatifyToken {
// 这⾥写的token要与测试公众号Token ⼀致
private static String token = "d2b37b3057e6edcb9a5cebc00f11e027";
@RequestMapping(value = "/vatToken/{appId}")
public String toVatifyToken(@PathVariable String appId, HttpServletRequest request, HttpServletResponse response) throws Exception {
String signature = request.getParameter("signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 随机字符串
String echostr = request.getParameter("echostr");
if (signature != null && checkSignature(signature, timestamp, nonce)) {
//token 校验过后放开以下代码即可
// response.setHeader("Content-type", "application/xml");
// Map<String, String> mapInfo = parseXml(request);
// JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(mapInfo));
// if (ObjectUtils.isNotEmpty(jsonObject)) {
// System.out.println("-------------appid-------" + appId);
// System.out.println("-------------解密信息明文-------" + jsonObject);
// String msgType = jsonObject.getString("MsgType");
// String toUserName = jsonObject.getString("ToUserName");
// String fromUserName = jsonObject.getString("FromUserName");
// String createTime = jsonObject.getString("CreateTime");
// String eventKey = jsonObject.getString("EventKey");
// String event = jsonObject.getString("Event");
// String ticket = jsonObject.getString("Ticket");
// System.out.println("-------------openid-------" + fromUserName);
// System.out.println("-------------msgType-------" + msgType);
// System.out.println("-------------eventKey-------" + eventKey);
// System.out.println("-------------event-------" + event);
// System.out.println("-------------ticket-------" + ticket);
// //回复,可以回复表情,蓝色可点击链接文字等
// String content = "你好啊";
// // 区分消息类型
// // 普通消息
// if ("text".equals(msgType)) { // 文本消息
// // todo 处理文本消息
// } else if ("image".equals(msgType)) { // 图片消息
// // todo 处理图片消息
// } else if ("voice".equals(msgType)) { //语音消息
// // todo 处理语音消息
// } else if ("video".equals(msgType)) { // 视频消息
// // todo 处理视频消息
// } else if ("shortvideo".equals(msgType)) { // 小视频消息
// // todo 处理小视频消息
// } else if ("location".equals(msgType)) { // 地理位置消息
// // todo 处理地理位置消息
// } else if ("link".equals(msgType)) { // 链接消息
// // todo 处理链接消息
// } else if ("subscribe".equals(event)) {//关注自动回复
// content = "[Party]终于等到你!欢迎你的关注!\n" +
// "\n" +
// "/:rose点击<a href=\"https://www.baidu.com\">【百度】</a>搜索更多优质内容\n";
// } else if ("unsubscribe".equals(event)) { // 取消订阅事件
// // todo 处理取消订阅事件
// } else if ("SCAN1".equals(event)) {
// content = "<a href='' data-miniprogram-path='/pages/home/index?orderId=WS220908164444014R6XDZF' data-miniprogram-appid='xxx'>滴滴打车丨天天低至8折></a>";
// } else if ("LOCATION".equals(event)) { // 上报地理位置事件
// // todo 处理上报地理位置事件
// } else if ("CLICK".equals(event)) { // 点击菜单拉取消息时的事件推送事件
// content = "<a href='' data-miniprogram-path='/pages/home/index?orderId=WS220908164444014R6XDZF' data-miniprogram-appid='xxx'>滴滴打车丨天天低至8折></a>";
// } else if ("VIEW".equals(event)) { // 点击菜单跳转链接时的事件推送
// // todo 处理点击菜单跳转链接时的事件推送
// } else {
//
// }
// response.setCharacterEncoding("UTF-8");
// response.getWriter().write(getXmlReturnMsg(fromUserName, toUserName, System.currentTimeMillis() / 1000, content));
// }
return echostr;
}
return null;
}
/**
* 构建普通消息
*
* @param toUser 接收方账号(openId)
* @param fromUser 开发者账号
* @param createTime 创建时间,整型
* @param content 内容
* @return 回复消息
*/
public String getXmlReturnMsg(String toUser, String fromUser, Long createTime, String content) {
return "<xml>\n" +
" <ToUserName><![CDATA[" + toUser + "]]></ToUserName>\n" +
" <FromUserName><![CDATA[" + fromUser + "]]></FromUserName>\n" +
" <CreateTime>" + createTime + "</CreateTime>\n" +
" <MsgType><![CDATA[text]]></MsgType>\n" +
" <Content><![CDATA[" + content + "]]></Content>\n" +
"</xml>";
}
/**
* dom4j 解析 xml 转换为 map
*
* @param request
* @return
* @throws Exception
*/
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList)
map.put(e.getName(), e.getText());
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
/**
* 验证签名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[]{token, timestamp, nonce};
// 将token、timestamp、nonce三个参数进⾏字典序排序
// Arrays.sort(arr);
sort(arr);
sort(arr);
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md = null;
String tmpStr = null;
try {
md = MessageDigest.getInstance("SHA-1");
// 将三个参数字符串拼接成⼀个字符串进⾏sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
content = null;
// 将sha1加密后的字符串可与signature对⽐,标识该请求来源于微信
return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}
/**
* 将字节数组转换为⼗六进制字符串
*
* @param byteArray
* @return
*/
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 将字节转换为⼗六进制字符串
*
* @param mByte
* @return
*/
private static String byteToHexStr(byte mByte) {
char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
public static void sort(String a[]) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[j].compareTo(a[i]) < 0) {
String temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
}
4、自定义菜单
接口地址
https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
引入依赖
<weixin-java.version>4.1.0</weixin-java.version>
<!--weixin-java-common-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-common</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<!--weixin-java-mp-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<!--weixin-java-open-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-open</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<!--weixin-java-miniapp-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>${weixin-java.version}</version>
</dependency>
sql:
CREATE TABLE `wx_menu` (
`id` varchar(32) NOT NULL COMMENT '菜单ID(click、scancode_push、scancode_waitmsg、pic_sysphoto、pic_photo_or_album、pic_weixin、location_select:保存key)',
`sort` int(11) DEFAULT '1' COMMENT '排序值',
`parent_id` varchar(32) DEFAULT NULL COMMENT '父菜单ID',
`app_id` varchar(32) DEFAULT NULL COMMENT '应用ID',
`type` char(20) DEFAULT NULL COMMENT '菜单类型click、view、miniprogram、scancode_push、scancode_waitmsg、pic_sysphoto、pic_photo_or_album、pic_weixin、location_select、media_id、view_limited等',
`name` varchar(20) DEFAULT NULL COMMENT '菜单名',
`url` varchar(500) DEFAULT NULL COMMENT 'view、miniprogram保存链接',
`ma_app_id` varchar(32) DEFAULT NULL COMMENT '小程序的appid',
`ma_page_path` varchar(100) DEFAULT NULL COMMENT '小程序的页面路径',
`rep_type` char(10) DEFAULT NULL COMMENT '回复消息类型(text:文本;image:图片;voice:语音;video:视频;music:音乐;news:图文)',
`rep_content` text COMMENT 'Text:保存文字',
`rep_media_id` varchar(64) DEFAULT NULL COMMENT 'imge、voice、news、video:mediaID',
`rep_name` varchar(100) DEFAULT NULL COMMENT '素材名、视频和音乐的标题',
`rep_desc` varchar(200) DEFAULT NULL COMMENT '视频和音乐的描述',
`rep_url` varchar(500) DEFAULT NULL COMMENT '链接',
`rep_hq_url` varchar(500) DEFAULT NULL COMMENT '高质量链接',
`rep_thumb_media_id` varchar(64) DEFAULT NULL COMMENT '缩略图的媒体id',
`rep_thumb_url` varchar(500) DEFAULT NULL COMMENT '缩略图url',
`content` mediumtext COMMENT '图文消息的内容',
`menu_rule_id` varchar(32) DEFAULT NULL COMMENT '菜单组ID',
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
`create_user` bigint(64) DEFAULT NULL COMMENT '创建人',
`create_dept` bigint(64) DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` bigint(64) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
`status` int(2) DEFAULT NULL COMMENT '状态1初始2已付款3部分退款4全额退款5订单关闭',
`is_deleted` int(2) DEFAULT '0' COMMENT '是否已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='自定义菜单表';
CREATE TABLE `wx_menu_rule` (
`id` varchar(32) NOT NULL DEFAULT '' COMMENT 'PK',
`app_id` varchar(32) DEFAULT NULL COMMENT 'appId',
`name` varchar(32) DEFAULT NULL COMMENT '名称',
`menu_type` char(2) CHARACTER SET utf8 DEFAULT NULL COMMENT '菜单类型(1:普通菜单,2:个性化菜单)',
`menu_id` varchar(32) DEFAULT NULL COMMENT 'menuid',
`tag_id` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '用户标签的id',
`sex` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '性别:男(1)女(2)',
`client_platform_type` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '客户端版本,当前只具体到系统型号:IOS(1), Android(2),Others(3)',
`country` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '国家信息',
`province` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '省份信息',
`city` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '城市信息',
`language` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '语言信息',
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
`create_user` bigint(64) DEFAULT NULL COMMENT '创建人',
`create_dept` bigint(64) DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` bigint(64) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
`status` int(2) DEFAULT NULL COMMENT '状态1初始2已付款3部分退款4全额退款5订单关闭',
`is_deleted` int(2) DEFAULT '0' COMMENT '是否已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='微信自定义菜单分组';
需要引入的代码
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import org.springblade.modules.manage.entity.WxConfig;
import org.springblade.modules.manage.service.IWxConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
/**
* 公众号Configuration
* @author www.joolun.com
*
*/
@Slf4j
@Configuration
public class WxMpConfiguration {
private static RedisTemplate redisTemplate;
private static IWxConfigService iWxConfigService;
@Autowired
public WxMpConfiguration( RedisTemplate redisTemplate, IWxConfigService iWxConfigService) {
this.redisTemplate = redisTemplate;
this.iWxConfigService = iWxConfigService;
}
/**
* 获取WxMpService
* @param appId
* @return
*/
public static WxMpService getMpService(String appId) {
WxMpService wxMpService = null;
WxConfig wxApp = iWxConfigService.getOne(Wrappers.<WxConfig>lambdaQuery().eq(WxConfig::getAppId, appId));
if(wxApp!=null && wxApp.getStatus()==1) {
if(wxApp.getIsComponent().equals("1")){//第三方授权账号
// wxMpService = WxOpenConfiguration.getOpenService().getWxOpenComponentService().getWxMpServiceByAppid(appId);
}else{
WxMpInRedisConfigStorage configStorage = new WxMpInRedisConfigStorage(redisTemplate);
configStorage.setAppId(wxApp.getAppId());
configStorage.setSecret(wxApp.getSecret());
configStorage.setToken(wxApp.getToken());
configStorage.setAesKey(wxApp.getAesKey());
wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(configStorage);
}
}
return wxMpService;
}
}
package org.springblade.modules.api.common.mp;
import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* 基于Redis的微信配置provider.
*
* @author www.joolun.com
*/
@SuppressWarnings("serial")
public class WxMpInRedisConfigStorage extends WxMpDefaultConfigImpl {
public static final String ACCESS_TOKEN_KEY = "wx:mp:access_token:";
public final static String JSAPI_TICKET_KEY = "wx:mp:jsapi_ticket:";
public final static String CARDAPI_TICKET_KEY = "wx:mp:cardapi_ticket:";
private final RedisTemplate<String, String> redisTemplate;
public WxMpInRedisConfigStorage(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
private String accessTokenKey;
private String jsapiTicketKey;
private String cardapiTicketKey;
/**
* 每个公众号生成独有的存储key.
*/
@Override
public void setAppId(String appId) {
super.setAppId(appId);
this.accessTokenKey = ACCESS_TOKEN_KEY.concat(appId);
this.jsapiTicketKey = JSAPI_TICKET_KEY.concat(appId);
this.cardapiTicketKey = CARDAPI_TICKET_KEY.concat(appId);
}
/**
*
* @param type
* @return
*/
private String getTicketRedisKey(TicketType type) {
return String.format("wx:mp:ticket:key:%s:%s", this.appId, type.getCode());
}
/**
*
* @return
*/
@Override
public String getAccessToken() {
return redisTemplate.opsForValue().get(this.accessTokenKey);
}
/**
*
* @return
*/
@Override
public boolean isAccessTokenExpired() {
return redisTemplate.getExpire(accessTokenKey) < 2;
}
/**
*
* @param accessToken
* @param expiresInSeconds
*/
@Override
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
redisTemplate.opsForValue().set(this.accessTokenKey, accessToken, expiresInSeconds - 200, TimeUnit.SECONDS);
}
/**
*
*/
@Override
public void expireAccessToken() {
redisTemplate.expire(this.accessTokenKey, 0, TimeUnit.SECONDS);
}
/**
*
* @param type
* @return
*/
@Override
public String getTicket(TicketType type) {
return redisTemplate.opsForValue().get(this.getTicketRedisKey(type));
}
/**
*
* @param type
* @return
*/
@Override
public boolean isTicketExpired(TicketType type) {
return redisTemplate.getExpire(this.getTicketRedisKey(type)) < 2;
}
/**
*
* @param type
* @param jsapiTicket
* @param expiresInSeconds
*/
@Override
public synchronized void updateTicket(TicketType type, String jsapiTicket, int expiresInSeconds) {
redisTemplate.opsForValue().set(this.getTicketRedisKey(type), jsapiTicket, expiresInSeconds - 200, TimeUnit.SECONDS);
}
/**
*
* @param type
*/
@Override
public void expireTicket(TicketType type) {
redisTemplate.expire(this.getTicketRedisKey(type), 0, TimeUnit.SECONDS);
}
/**
*
* @return
*/
@Override
public String getJsapiTicket() {
return redisTemplate.opsForValue().get(this.jsapiTicketKey);
}
/**
*
* @return
*/
@Override
public String getCardApiTicket() {
return redisTemplate.opsForValue().get(cardapiTicketKey);
}
}
package org.springblade.modules.manage.controller;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springblade.common.constant.WxReturnCode;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.log.annotation.ApiLog;
import org.springblade.core.tool.api.R;
import org.springblade.modules.manage.service.IWxMenuService;
import org.springframework.web.bind.annotation.*;
/**
* 自定义菜单表 控制器
*
* @author BladeX
* @since 2021-10-25
*/
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("weixin/wxmenu")
@Api(value = "自定义菜单表", tags = "自定义菜单表接口")
public class WxMenuController extends BladeController {
private final IWxMenuService wxMenuService;
/**
* 通过appId查询自定义菜单
*
* @param appId
* @param menuRuleId
* @return R
*/
@ApiOperation(value = "通过appId查询自定义菜单")
@GetMapping("/list")
public R getWxMenuButton(String appId, String menuRuleId) {
return R.data(wxMenuService.getWxMenuButton(appId, menuRuleId));
}
/**
* 保存并发布菜单
*
* @param
* @return R
*/
@ApiOperation(value = "保存并发布菜单")
@ApiLog("保存并发布菜单")
@PostMapping("/release")
public R saveAndRelease(@RequestBody String data) {
JSONObject jSONObject = JSONUtil.parseObj(data);
String strWxMenu = jSONObject.getStr("strWxMenu");
String appId = jSONObject.getStr("appId");
try {
return R.data(wxMenuService.saveAndRelease(appId, strWxMenu));
} catch (WxErrorException e) {
e.printStackTrace();
log.error("发布自定义菜单失败appID:" + appId + ":" + e.getMessage());
return WxReturnCode.wxErrorExceptionHandler(e);
}
}
/**
* 删除菜单
*
* @param ruleId
* @return R
*/
@ApiOperation(value = "删除菜单")
@ApiLog("删除微信菜单")
@DeleteMapping("/{ruleId}")
public R removeByRuleId(@PathVariable String ruleId) {
try {
wxMenuService.removeByRuleId(ruleId);
return R.success("操作成功");
} catch (WxErrorException e) {
e.printStackTrace();
log.error("删除微信菜单失败ruleId:" + ruleId + ":" + e.getMessage());
return WxReturnCode.wxErrorExceptionHandler(e);
}
}
}
package org.springblade.modules.manage.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springblade.modules.manage.entity.WxMenu;
import org.springblade.modules.manage.entity.WxMenuRule;
import org.springblade.modules.manage.vo.WxMenuVO;
import java.io.Serializable;
/**
* 自定义菜单表 服务类
*
* @author BladeX
* @since 2021-10-25
*/
public interface IWxMenuService extends IService<WxMenu> {
/**
* 自定义分页
*
* @param page
* @param wxMenu
* @return
*/
IPage<WxMenuVO> selectWxMenuPage(IPage<WxMenuVO> page, WxMenuVO wxMenu);
/***
* 获取WxApp下的菜单
* @param appId
* @param menuRuleId
* @return
*/
String getWxMenuButton(String appId, String menuRuleId);
/**
* 保存并发布菜单
* @param
*/
WxMenuRule saveAndRelease(String appId, String strWxMenu) throws WxErrorException;
/**
* 删除菜单
* @param ruleId
* @return
*/
void removeByRuleId(Serializable ruleId) throws WxErrorException;
}
package org.springblade.modules.manage.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.AllArgsConstructor;
import me.chanjar.weixin.common.error.WxErrorException;
import org.apache.commons.lang3.StringUtils;
import org.springblade.common.constant.CommonConstants;
import org.springblade.common.constant.ConfigConstant;
import org.springblade.modules.api.common.mp.WxMpConfiguration;
import org.springblade.modules.manage.entity.*;
import org.springblade.modules.manage.mapper.WxMenuMapper;
import org.springblade.modules.manage.service.IWxConfigService;
import org.springblade.modules.manage.service.IWxMenuRuleService;
import org.springblade.modules.manage.service.IWxMenuService;
import org.springblade.modules.manage.vo.WxMenuVO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义菜单表 服务实现类
*
* @author BladeX
* @since 2021-10-25
*/
@Service
@AllArgsConstructor
public class WxMenuServiceImpl extends ServiceImpl<WxMenuMapper, WxMenu> implements IWxMenuService {
@Override
public IPage<WxMenuVO> selectWxMenuPage(IPage<WxMenuVO> page, WxMenuVO wxMenu) {
return page.setRecords(baseMapper.selectWxMenuPage(page, wxMenu));
}
private final IWxConfigService iWxConfigService;
private final IWxMenuRuleService wxMenuRuleService;
/***
* 获取WxApp下的菜单树结构
* @param appId
* @return
*/
@Override
public String getWxMenuButton(String appId, String menuRuleId) {
MenuMp menu = new MenuMp();
//查询菜单组
WxMenuRule wxMenuRule = wxMenuRuleService.getById(menuRuleId);
if(wxMenuRule == null){
return null;
}
MenuRule menuRule = new MenuRule();
BeanUtil.copyProperties(wxMenuRule,menuRule);
menuRule.setTag_id(wxMenuRule.getTagId());
menu.setMatchrule(menuRule);
//查出一级菜单
List<WxMenu> listWxMenu = baseMapper.selectList(Wrappers
.<WxMenu>query().lambda()
.eq(WxMenu::getAppId, appId)
.eq(WxMenu::getParentId, CommonConstants.PARENT_ID)
.eq(WxMenu::getMenuRuleId, menuRuleId)
.orderByAsc(WxMenu::getSort));
List<MenuButton> listMenuButton = new ArrayList<>();
MenuButton menuButton;
List<MenuButton> subButtons;
MenuButton subButton;
if(listWxMenu!=null&&listWxMenu.size()>0){
for(WxMenu wxMenu : listWxMenu){
menuButton = new MenuButton();
menuButton.setName(wxMenu.getName());
String type = wxMenu.getType();
if(StringUtils.isNotBlank(type)){//无二级菜单
menuButton.setType(type);
setButtonValue(menuButton,wxMenu);
}else{//有二级菜单
//查出二级菜单
List<WxMenu> listWxMenu1 = baseMapper.selectList(Wrappers
.<WxMenu>query().lambda()
.eq(WxMenu::getAppId, appId)
.eq(WxMenu::getParentId,wxMenu.getId())
.eq(WxMenu::getMenuRuleId, menuRuleId)
.orderByAsc(WxMenu::getSort));
subButtons = new ArrayList<>();
for(WxMenu wxMenu1 : listWxMenu1){
subButton = new MenuButton();
String type1 = wxMenu1.getType();
subButton.setName(wxMenu1.getName());
subButton.setType(type1);
setButtonValue(subButton,wxMenu1);
subButtons.add(subButton);
}
menuButton.setSub_button(subButtons);
}
listMenuButton.add(menuButton);
}
}
menu.setButton(listMenuButton);
return menu.toString();
}
void setButtonValue(MenuButton menuButton,WxMenu wxMenu){
menuButton.setKey(wxMenu.getId());
menuButton.setUrl(wxMenu.getUrl());
// menuButton.setContent(wxMenu.getContent());
menuButton.setRepContent(wxMenu.getRepContent());
menuButton.setMedia_id(wxMenu.getRepMediaId());
menuButton.setRepType(wxMenu.getRepType());
menuButton.setRepName(wxMenu.getRepName());
menuButton.setAppid(wxMenu.getMaAppId());
menuButton.setPagepath(wxMenu.getMaPagePath());
menuButton.setUrl(wxMenu.getUrl());
menuButton.setRepUrl(wxMenu.getRepUrl());
menuButton.setRepHqUrl(wxMenu.getRepHqUrl());
menuButton.setRepDesc(wxMenu.getRepDesc());
menuButton.setRepThumbMediaId(wxMenu.getRepThumbMediaId());
menuButton.setRepThumbUrl(wxMenu.getRepThumbUrl());
}
/**
* 保存并发布菜单
* @param
* {
* "appId":"wx54b08bd2fcb3a25b",
* "strWxMenu":{
* "matchrule":{
* "tag_id":1,
* "menuType":"1",
* "name":"测试菜单"
* },
* "button":[
* {
* "type":"click",
* "name":"测试菜单",
* "key":"V1001_TODAY_MUSIC"
* },
* {
* "name":"菜单",
* "sub_button":[
* {
* "type":"click",
* "name":"赞一下我们",
* "key":"V1001_GOOD"
* }]
* }]
* }
* }
*/
@Override
@Transactional(rollbackFor = Exception.class)
public WxMenuRule saveAndRelease(String appId , String strWxMenu) throws WxErrorException {
MenuMp menu = MenuMp.fromJson(strWxMenu);
MenuRule menuRule = menu.getMatchrule();
if(menuRule != null){
WxMenuRule wxMenuRule = new WxMenuRule();
BeanUtil.copyProperties(menuRule,wxMenuRule);
wxMenuRule.setTagId(menuRule.getTag_id());
//保存或修改菜单分组
wxMenuRule.setAppId(appId);
wxMenuRuleService.saveOrUpdate(wxMenuRule);
List<MenuButton> buttons = menu.getButton();
if(StrUtil.isNotBlank(menuRule.getId())){
//先删除菜单
baseMapper.delete(Wrappers
.<WxMenu>query().lambda()
.eq(WxMenu::getAppId, appId)
.eq(WxMenu::getMenuRuleId,wxMenuRule.getId()));
if(ConfigConstant.WX_MENU_TYPE_2.equals(wxMenuRule.getMenuType())){
//删除微信已有个性化菜单
wxMenuRule = wxMenuRuleService.getById(wxMenuRule.getId());
WxMpConfiguration.getMpService(wxMenuRule.getAppId()).getMenuService().menuDelete(wxMenuRule.getMenuId());
}
}
WxConfig wxApp = iWxConfigService.getOne(Wrappers.<WxConfig>lambdaQuery().eq(WxConfig::getAppId, appId));
WxMenu wxMenu = null;
WxMenu wxMenu1 = null;
int sort1 = 1;
int sort2 = 1;
//入库菜单
for(MenuButton menuButton : buttons){
wxMenu = new WxMenu();
setWxMenuValue(wxMenu,menuButton,wxApp);
wxMenu.setSort(sort1);
wxMenu.setParentId(CommonConstants.PARENT_ID);
wxMenu.setMenuRuleId(wxMenuRule.getId());
baseMapper.insert(wxMenu);
menuButton.setKey(wxMenu.getId());
sort1++;
for(MenuButton menuButton1 : menuButton.getSub_button()){
wxMenu1 = new WxMenu();
setWxMenuValue(wxMenu1,menuButton1,wxApp);
wxMenu1.setSort(sort2);
wxMenu1.setParentId(wxMenu.getId());
wxMenu1.setMenuRuleId(wxMenuRule.getId());
baseMapper.insert(wxMenu1);
menuButton1.setKey(wxMenu1.getId());
sort2++;
}
}
//创建菜单
if(ConfigConstant.WX_MENU_TYPE_1.equals(wxMenuRule.getMenuType())){
menu.setMatchrule(null);
}
String menuId = WxMpConfiguration.getMpService(appId).getMenuService().menuCreate(menu.toString());
//个性化菜单保存菜单id
if(ConfigConstant.WX_MENU_TYPE_2.equals(wxMenuRule.getMenuType())){
wxMenuRule.setMenuId(menuId);
wxMenuRuleService.updateById(wxMenuRule);
}
return wxMenuRule;
}
return null;
}
void setWxMenuValue(WxMenu wxMenu,MenuButton menuButton,WxConfig wxConfig){
wxMenu.setId(menuButton.getKey());
wxMenu.setAppId(wxConfig.getAppId());
wxMenu.setType(menuButton.getType());
wxMenu.setName(menuButton.getName());
wxMenu.setUrl(menuButton.getUrl());
wxMenu.setRepMediaId(menuButton.getMedia_id());
wxMenu.setRepType(menuButton.getRepType());
wxMenu.setRepName(menuButton.getRepName());
wxMenu.setMaAppId(menuButton.getAppid());
wxMenu.setMaPagePath(menuButton.getPagepath());
wxMenu.setRepContent(menuButton.getRepContent());
// wxMenu.setContent(menuButton.getContent());
wxMenu.setRepUrl(menuButton.getRepUrl());
wxMenu.setRepHqUrl(menuButton.getRepHqUrl());
wxMenu.setRepDesc(menuButton.getRepDesc());
wxMenu.setRepThumbMediaId(menuButton.getRepThumbMediaId());
wxMenu.setRepThumbUrl(menuButton.getRepThumbUrl());
menuButton.setRepUrl(null);
menuButton.setRepDesc(null);
menuButton.setRepHqUrl(null);
menuButton.setContent(null);
menuButton.setRepContent(null);
menuButton.setRepType(null);
menuButton.setRepName(null);
menuButton.setRepThumbMediaId(null);
menuButton.setRepThumbUrl(null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeByRuleId(Serializable ruleId) throws WxErrorException {
WxMenuRule wxMenuRule = wxMenuRuleService.getById(ruleId);
if(ConfigConstant.WX_MENU_TYPE_1.equals(wxMenuRule.getMenuType())){//删除普通菜单,要删除所有个性化菜单
//数据库删除
wxMenuRuleService.remove(Wrappers
.<WxMenuRule>query().lambda()
.eq(WxMenuRule::getAppId, wxMenuRule.getAppId()));
baseMapper.delete(Wrappers
.<WxMenu>query().lambda()
.eq(WxMenu::getAppId, wxMenuRule.getAppId()));
//微信删除
WxMpConfiguration.getMpService(wxMenuRule.getAppId()).getMenuService().menuDelete();
}else{
//数据库删除
wxMenuRuleService.removeById(wxMenuRule.getId());
baseMapper.delete(Wrappers
.<WxMenu>query().lambda()
.eq(WxMenu::getAppId, wxMenuRule.getAppId())
.eq(WxMenu::getMenuRuleId,wxMenuRule.getId()));
//微信删除
WxMpConfiguration.getMpService(wxMenuRule.getAppId()).getMenuService().menuDelete(wxMenuRule.getMenuId());
}
}
}
/**
* 自定义菜单表 Mapper 接口
*
* @author BladeX
* @since 2021-10-25
*/
public interface WxMenuMapper extends BaseMapper<WxMenu> {
/**
* 自定义分页
*
* @param page
* @param wxMenu
* @return
*/
List<WxMenuVO> selectWxMenuPage(IPage page, WxMenuVO wxMenu);
}
<?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.springblade.modules.manage.mapper.WxMenuMapper">
<!-- 通用查询映射结果 -->
<resultMap id="wxMenuResultMap" type="org.springblade.modules.manage.entity.WxMenu">
<result column="id" property="id"/>
<result column="create_user" property="createUser"/>
<result column="create_dept" property="createDept"/>
<result column="create_time" property="createTime"/>
<result column="update_user" property="updateUser"/>
<result column="update_time" property="updateTime"/>
<result column="status" property="status"/>
<result column="is_deleted" property="isDeleted"/>
<result column="sort" property="sort"/>
<result column="parent_id" property="parentId"/>
<result column="app_id" property="appId"/>
<result column="type" property="type"/>
<result column="name" property="name"/>
<result column="url" property="url"/>
<result column="ma_app_id" property="maAppId"/>
<result column="ma_page_path" property="maPagePath"/>
<result column="rep_type" property="repType"/>
<result column="rep_content" property="repContent"/>
<result column="rep_media_id" property="repMediaId"/>
<result column="rep_name" property="repName"/>
<result column="rep_desc" property="repDesc"/>
<result column="rep_url" property="repUrl"/>
<result column="rep_hq_url" property="repHqUrl"/>
<result column="rep_thumb_media_id" property="repThumbMediaId"/>
<result column="rep_thumb_url" property="repThumbUrl"/>
<!-- <result column="content" property="content"/>-->
<result column="menu_rule_id" property="menuRuleId"/>
</resultMap>
<select id="selectWxMenuPage" resultMap="wxMenuResultMap">
select * from wx_menu where is_deleted = 0
</select>
</mapper>
用到的实体类vo
package org.springblade.modules.manage.entity;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.apache.ibatis.type.JdbcType;
import org.springblade.common.handler.JsonTypeHandler;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* 自定义菜单表实体类
*
* @author BladeX
* @since 2021-10-25
*/
@Data
@ApiModel(value = "WxMenu对象", description = "自定义菜单表")
public class WxMenu implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键id")
@TableId(value = "id",type = IdType.ASSIGN_ID)
private String id;
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty("创建人")
private Long createUser;
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty("创建部门")
private Long createDept;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("创建时间")
private Date createTime;
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty("更新人")
private Long updateUser;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("更新时间")
private Date updateTime;
@ApiModelProperty("业务状态")
private Integer status;
@ApiModelProperty("是否已删除")
private Integer isDeleted;
@ApiModelProperty("租户ID")
private String tenantId;
/**
* 排序值
*/
@ApiModelProperty(value = "排序值")
private Integer sort;
/**
* 父菜单ID
*/
@ApiModelProperty(value = "父菜单ID")
private String parentId;
/**
* 应用ID
*/
@ApiModelProperty(value = "应用ID")
private String appId;
/**
* 菜单类型click、view、miniprogram、scancode_push、scancode_waitmsg、pic_sysphoto、pic_photo_or_album、pic_weixin、location_select、media_id、view_limited等
*/
@ApiModelProperty(value = "菜单类型click、view、miniprogram、scancode_push、scancode_waitmsg、pic_sysphoto、pic_photo_or_album、pic_weixin、location_select、media_id、view_limited等")
private String type;
/**
* 菜单名
*/
@ApiModelProperty(value = "菜单名")
private String name;
/**
* view、miniprogram保存链接
*/
@ApiModelProperty(value = "view、miniprogram保存链接")
private String url;
/**
* 小程序的appid
*/
@ApiModelProperty(value = "小程序的appid")
private String maAppId;
/**
* 小程序的页面路径
*/
@ApiModelProperty(value = "小程序的页面路径")
private String maPagePath;
/**
* 回复消息类型(text:文本;image:图片;voice:语音;video:视频;music:音乐;news:图文)
*/
@ApiModelProperty(value = "回复消息类型(text:文本;image:图片;voice:语音;video:视频;music:音乐;news:图文)")
private String repType;
/**
* Text:保存文字
*/
@ApiModelProperty(value = "Text:保存文字")
private String repContent;
/**
* imge、voice、news、video:mediaID
*/
@ApiModelProperty(value = "imge、voice、news、video:mediaID")
private String repMediaId;
/**
* 素材名、视频和音乐的标题
*/
@ApiModelProperty(value = "素材名、视频和音乐的标题")
private String repName;
/**
* 视频和音乐的描述
*/
@ApiModelProperty(value = "视频和音乐的描述")
private String repDesc;
/**
* 链接
*/
@ApiModelProperty(value = "链接")
private String repUrl;
/**
* 高质量链接
*/
@ApiModelProperty(value = "高质量链接")
private String repHqUrl;
/**
* 缩略图的媒体id
*/
@ApiModelProperty(value = "缩略图的媒体id")
private String repThumbMediaId;
/**
* 缩略图url
*/
@ApiModelProperty(value = "缩略图url")
private String repThumbUrl;
/**
* 图文消息的内容
*/
@TableField(typeHandler = JsonTypeHandler.class)
// private JSONObject content;
/**
* 菜单组ID
*/
@ApiModelProperty(value = "菜单组ID")
private String menuRuleId;
}
package org.springblade.modules.manage.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* 微信自定义菜单分组实体类
*
* @author BladeX
* @since 2021-10-25
*/
@Data
@ApiModel(value = "WxMenuRule对象", description = "微信自定义菜单分组")
public class WxMenuRule implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键id")
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty("创建人")
private Long createUser;
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty("创建部门")
private Long createDept;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("创建时间")
private Date createTime;
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty("更新人")
private Long updateUser;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("更新时间")
private Date updateTime;
@ApiModelProperty("业务状态")
private Integer status;
@TableLogic
@ApiModelProperty("是否已删除")
private Integer isDeleted;
@ApiModelProperty("租户ID")
private String tenantId;
/**
* appId
*/
@ApiModelProperty(value = "appId")
private String appId;
/**
* 名称
*/
@ApiModelProperty(value = "名称")
private String name;
/**
* 菜单类型(1:普通菜单,2:个性化菜单)
*/
@ApiModelProperty(value = "菜单类型(1:普通菜单,2:个性化菜单)")
private String menuType;
/**
* menuid
*/
@ApiModelProperty(value = "menuid")
private String menuId;
/**
* 用户标签的id
*/
@ApiModelProperty(value = "用户标签的id")
private String tagId;
/**
* 性别:男(1)女(2)
*/
@ApiModelProperty(value = "性别:男(1)女(2)")
private String sex;
/**
* 客户端版本,当前只具体到系统型号:IOS(1), Android(2),Others(3)
*/
@ApiModelProperty(value = "客户端版本,当前只具体到系统型号:IOS(1), Android(2),Others(3)")
private String clientPlatformType;
/**
* 国家信息
*/
@ApiModelProperty(value = "国家信息")
private String country;
/**
* 省份信息
*/
@ApiModelProperty(value = "省份信息")
private String province;
/**
* 城市信息
*/
@ApiModelProperty(value = "城市信息")
private String city;
/**
* 语言信息
*/
@ApiModelProperty(value = "语言信息")
private String language;
}
package org.springblade.modules.manage.entity;
import lombok.Data;
/**
* 微信自定义菜单分组
*
* @author www.joolun.com
* @date 2020-02-22 19:34:22
*/
@Data
public class MenuRule {
private static final long serialVersionUID=1L;
/**
* 用户标签的id
*/
private String tag_id;
private String id;
private String menuType;
private String name;
}
package org.springblade.modules.manage.entity;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义菜单模型
*
* @author www.joolun.com
*/
@Data
public class MenuMp implements Serializable {
private static final long serialVersionUID = -7083914585539687746L;
private List<MenuButton> button = new ArrayList<>();
private MenuRule matchrule;
/**
* 反序列化
*/
public static MenuMp fromJson(String json) {
return JSONUtil.parseObj(json).toBean(MenuMp.class);
}
public String toJson() {
return JSONUtil.toJsonStr(this);
}
@Override
public String toString() {
return this.toJson();
}
}
package org.springblade.modules.manage.entity;
import cn.hutool.json.JSONObject;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义菜单模型
*
* @author www.joolun.com
*/
@Data
public class MenuButton implements Serializable {
private String type;
private String name;
private String key;
private String url;
private String media_id;
private String appid;
private String pagepath;
private List<MenuButton> sub_button = new ArrayList();
/**
* content内容
*/
private JSONObject content;
private String repContent;
/**
* 消息类型
*/
private String repType;
/**
* 消息名
*/
private String repName;
/**
* 视频和音乐的描述
*/
private String repDesc;
/**
* 视频和音乐的描述
*/
private String repUrl;
/**
* 高质量链接
*/
private String repHqUrl;
/**
* 缩略图的媒体id
*/
private String repThumbMediaId;
/**
* 缩略图url
*/
private String repThumbUrl;
}
package org.springblade.modules.manage.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.modules.manage.entity.WxMenuRule;
/**
* 微信自定义菜单分组视图实体类
*
* @author BladeX
* @since 2021-10-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "WxMenuRuleVO对象", description = "微信自定义菜单分组")
public class WxMenuRuleVO extends WxMenuRule {
private static final long serialVersionUID = 1L;
}
package org.springblade.modules.manage.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.modules.manage.entity.WxMenu;
/**
* 自定义菜单表视图实体类
*
* @author BladeX
* @since 2021-10-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "WxMenuVO对象", description = "自定义菜单表")
public class WxMenuVO extends WxMenu {
private static final long serialVersionUID = 1L;
}
保存菜单请求数据格式
{
"appId":"wx54b08bd2fcbxxxx",
"strWxMenu":{
"matchrule":{
"id":"1572852254619709441",
"tag_id":2,
"menuType":"1",
"name":"测试"
},
"button":[
{
"type": "scancode_push",
"name": "扫扫",
"key": "V1004_TODAY_SCAN",
"sub_button": [ ],
"url":"http://www.soso.com/"
},
{
"name":"菜单",
"sub_button":[
{
"type":"view",
"name":"测试",
"key":"V1001_GOOD",
"url":"http://www.soso.com/"
},
{
"type":"click",
"name":"点击推送",
"key":"V1002_CLICK",
"url":"http://www.soso.com/"
}
]
}
]
}
}
实现效果
v: OurBestTime