mp:完成 menu 点击时,自动回复消息的逻辑

This commit is contained in:
YunaiV 2023-01-07 11:07:02 +08:00
parent 665f4b2b09
commit 71beeabe9c
11 changed files with 150 additions and 160 deletions

View File

@ -16,5 +16,19 @@ tenant-id: {{adminTenentId}}
"name":"搜索",
"type":"view",
"url":"http://www.soso.com/"
},
{
"name": "父按钮",
"subButtons": [
{
"type":"click",
"name":"归去来兮",
"key":"MUSIC"
},
{
"name":"不说",
"type":"view",
"url":"https://www.soso.com/"
}]
}]
}

View File

@ -18,7 +18,4 @@ public class MpMenuBaseVO {
@NotNull(message = "公众号账号的编号不能为空")
private Long accountId;
@NotNull(message = "按钮不能为空")
private List<WxMenuButton> buttons;
}

View File

@ -2,6 +2,10 @@ package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
import lombok.*;
import io.swagger.annotations.*;
import me.chanjar.weixin.common.bean.menu.WxMenuButton;
import javax.validation.constraints.NotNull;
import java.util.List;
@ApiModel("管理后台 - 微信菜单保存 Request VO")
@Data
@ -9,4 +13,7 @@ import io.swagger.annotations.*;
@ToString(callSuper = true)
public class MpMenuSaveReqVO extends MpMenuBaseVO {
@NotNull(message = "按钮不能为空")
private List<WxMenuButton> buttons;
}

View File

@ -2,7 +2,10 @@ package cn.iocoder.yudao.module.mp.convert.menu;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.*;
import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
@ -16,4 +19,16 @@ public interface MpMenuConvert {
MpMenuRespVO convert(MpMenuDO bean);
@Mappings({
@Mapping(source = "menu.appId", target = "appId"),
@Mapping(source = "menu.replyMessageType", target = "type"),
@Mapping(source = "menu.replyContent", target = "content"),
@Mapping(source = "menu.replyMediaId", target = "mediaId"),
@Mapping(source = "menu.replyMediaUrl", target = "mediaUrl"),
@Mapping(source = "menu.replyTitle", target = "title"),
@Mapping(source = "menu.replyDescription", target = "description"),
@Mapping(source = "menu.replyArticles", target = "articles"),
})
MpMessageSendOutReqBO convert(String openid, MpMenuDO menu);
}

View File

@ -20,7 +20,7 @@ public interface MpAutoReplyConvert {
@Mapping(source = "reply.responseMediaUrl", target = "mediaUrl"),
@Mapping(source = "reply.responseTitle", target = "title"),
@Mapping(source = "reply.responseDescription", target = "description"),
@Mapping(source = "reply.responseArticle", target = "article"),
@Mapping(source = "reply.responseArticles", target = "articles"),
})
MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply);

View File

@ -64,7 +64,7 @@ public interface MpMessageConvert {
.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription());
break;
case WxConsts.XmlMsgType.NEWS: // 5. 图文
message.setArticles(Collections.singletonList(sendReqBO.getArticle()));
message.setArticles(sendReqBO.getArticles());
break;
default:
throw new IllegalArgumentException("不支持的消息类型:" + message.getType());

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.mp.dal.dataobject.menu;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
@ -8,7 +7,6 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ -20,8 +18,6 @@ import java.util.List;
/**
* 微信菜单 DO
*
* 一个公众号只有一个 MpMenuDO 记录一个公众号的多个菜单对应到就是 {@link #buttons} 多个按钮
*
* @author 芋道源码
*/
@TableName(value = "mp_menu", autoResultMap = true)
@ -32,7 +28,12 @@ import java.util.List;
public class MpMenuDO extends BaseDO {
/**
* 主键
* 编号 - 顶级菜单
*/
public static final Long ID_ROOT = 0L;
/**
* 编号
*/
@TableId
private Long id;
@ -50,127 +51,105 @@ public class MpMenuDO extends BaseDO {
private String appId;
/**
* 按钮列表
* 菜单名称
*/
@TableField(typeHandler = ButtonTypeHandler.class)
private List<Button> buttons;
private String name;
/**
* 同步状态
* 菜单标识
*
* true - 已同步
* false - 未同步
* 支持多 DB 类型时无法直接使用 key + @TableField("menuKey") 来实现转换原因是 "menuKey" AS key 而存在报错
*/
private Boolean syncStatus;
private String menuKey;
/**
* 父菜单编号
*/
private Long parentId;
/**
* 排序
*/
private Integer sort;
// ========== 按钮操作 ==========
/**
* 按钮
* 按钮类型
*
* 枚举 {@link MenuButtonType}
*/
@Data
public static class Button {
private String type;
/**
* 类型
*
* 枚举 {@link MenuButtonType}
*/
private String type;
/**
* 消息类型
*
* {@link #type} CLICKSCANCODE_WAITMSG
*
* 枚举 {@link WxConsts.XmlMsgType} 中的 TEXTIMAGEVOICEVIDEONEWS
*/
private String messageType;
/**
* 名称
*/
private String name;
/**
* 标识
*/
private String key;
/**
* 二级菜单列表
*/
private List<Button> subButtons;
/**
* 网页链接
*
* 用户点击菜单可打开链接不超过 1024 字节
*
* 类型为 {@link WxConsts.XmlMsgType} VIEWMINIPROGRAM
*/
private String url;
/**
* 网页链接
*
* 用户点击菜单可打开链接不超过 1024 字节
*
* 类型为 {@link WxConsts.XmlMsgType} VIEWMINIPROGRAM
*/
private String url;
/**
* 小程序的 appId
*
* 类型为 {@link WxConsts.XmlMsgType} MINIPROGRAM
*/
private String appId;
/**
* 小程序的 appId
*
* 类型为 {@link MenuButtonType} MINIPROGRAM
*/
private String miniProgramAppId;
/**
* 小程序的页面路径
*
* 类型为 {@link MenuButtonType} MINIPROGRAM
*/
private String miniProgramPagePath;
/**
* 小程序的页面路径
*
* 类型为 {@link WxConsts.XmlMsgType} MINIPROGRAM
*/
private String pagePath;
// ========== 消息内容 ==========
/**
* 消息内容
*
* 消息类型为 {@link WxConsts.XmlMsgType} TEXT
*/
private String content;
/**
* 消息类型
*
* {@link #type} CLICKSCANCODE_WAITMSG
*
* 枚举 {@link WxConsts.XmlMsgType} 中的 TEXTIMAGEVOICEVIDEONEWS
*/
private String replyMessageType;
/**
* 媒体 id
*
* 消息类型为 {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
private String mediaId;
/**
* 媒体 URL
*
* 消息类型为 {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
private String mediaUrl;
/**
* 回复的消息内容
*
* 消息类型为 {@link WxConsts.XmlMsgType} TEXT
*/
private String replyContent;
/**
* 回复的标题
*
* 消息类型为 {@link WxConsts.XmlMsgType} VIDEO
*/
private String title;
/**
* 回复的描述
*
* 消息类型为 {@link WxConsts.XmlMsgType} VIDEO
*/
private String description;
/**
* 回复的媒体 id
*
* 消息类型为 {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
private String replyMediaId;
/**
* 回复的媒体 URL
*
* 消息类型为 {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
private String replyMediaUrl;
/**
* 图文消息
*
* 消息类型为 {@link WxConsts.XmlMsgType} NEWS
*/
private MpMessageDO.Article article;
/**
* 回复的标题
*
* 消息类型为 {@link WxConsts.XmlMsgType} VIDEO
*/
private String replyTitle;
/**
* 回复的描述
*
* 消息类型为 {@link WxConsts.XmlMsgType} VIDEO
*/
private String replyDescription;
}
/**
* 回复的图文消息数组
*
* 消息类型为 {@link WxConsts.XmlMsgType} NEWS
*/
@TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class)
private List<MpMessageDO.Article> replyArticles;
// TODO @芋艿可以找一些新的思路
public static class ButtonTypeHandler extends AbstractJsonTypeHandler<List<Button>> {
@Override
protected List<Button> parse(String json) {
return JsonUtils.parseArray(json, Button.class);
}
@Override
protected String toJson(List<Button> obj) {
return JsonUtils.toJsonString(obj);
}
}
}

View File

@ -16,6 +16,7 @@ import lombok.ToString;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
import java.util.List;
import java.util.Set;
/**
@ -130,6 +131,6 @@ public class MpAutoReplyDO extends BaseDO {
*
* 消息类型为 {@link WxConsts.XmlMsgType} NEWS
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private MpMessageDO.Article responseArticle;
@TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class)
private List<MpMessageDO.Article> responseArticles;
}

View File

@ -7,8 +7,9 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MpMenuMapper extends BaseMapperX<MpMenuDO> {
default MpMenuDO selectByAppId(String appId) {
return selectOne(MpMenuDO::getAppId, appId);
default MpMenuDO selectByAppIdAndMenuKey(String appId, String menuKey) {
return selectOne(MpMenuDO::getAppId, appId,
MpMenuDO::getMenuKey, menuKey);
}
}

View File

@ -1,13 +1,11 @@
package cn.iocoder.yudao.module.mp.service.menu;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mp.convert.menu.MpMenuConvert;
import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
import cn.iocoder.yudao.module.mp.service.message.MpMessageService;
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.menu.WxMenu;
import me.chanjar.weixin.common.error.WxErrorException;
@ -37,6 +35,9 @@ import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*;
@Slf4j
public class MpMenuServiceImpl implements MpMenuService {
@Resource
private MpMessageService mpMessageService;
@Resource
@Lazy // 延迟加载避免循环引用报错
private MpServiceFactory mpServiceFactory;
@ -87,47 +88,21 @@ public class MpMenuServiceImpl implements MpMenuService {
@Override
public WxMpXmlOutMessage reply(String appId, String key, String openid) {
// 获得菜单
MpMenuDO menu = mpMenuMapper.selectByAppId(appId);
// 第一步获得菜单
MpMenuDO menu = mpMenuMapper.selectByAppIdAndMenuKey(appId, key);
if (menu == null) {
log.error("[reply][appId({}) 找不到对应的菜单]", appId);
return null;
}
// 匹配对应的按钮
MpMenuDO.Button button = getMenuButton(menu, key);
if (button == null) {
log.error("[reply][appId({}) key({}) 找不到对应的菜单按钮]", appId, key);
log.error("[reply][appId({}) key({}) 找不到对应的菜单]", appId, key);
return null;
}
// 按钮必须要有消息类型不然后续无法回复消息
if (StrUtil.isEmpty(button.getMessageType())) {
log.error("[reply][appId({}) key({}) 不存在消息类型({})]", appId, key, button);
if (StrUtil.isEmpty(menu.getReplyMessageType())) {
log.error("[reply][menu({}) 不存在对应的消息类型]", menu);
return null;
}
// 回复消息
return null;
}
private MpMenuDO.Button getMenuButton(MpMenuDO menu, String key) {
// 先查询子按钮
for (MpMenuDO.Button button : menu.getButtons()) {
if (CollUtil.isEmpty(button.getSubButtons())) {
continue;
}
for (MpMenuDO.Button subButton : button.getSubButtons()) {
if (StrUtil.equals(subButton.getKey(), key)) {
return subButton;
}
}
}
// 再查询父按钮
for (MpMenuDO.Button button : menu.getButtons()) {
if (StrUtil.equals(button.getKey(), key)) {
return button;
}
}
return null;
// 第二步回复消息
MpMessageSendOutReqBO sendReqBO = MpMenuConvert.INSTANCE.convert(openid, menu);
return mpMessageService.sendOutMessage(sendReqBO);
}
}

View File

@ -9,6 +9,7 @@ import me.chanjar.weixin.common.api.WxConsts;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 公众号消息发送 Request BO
@ -85,6 +86,6 @@ public class MpMessageSendOutReqBO {
*/
@Valid
@NotNull(message = "图文消息不能为空", groups = NewsGroup.class)
private MpMessageDO.Article article;
private List<MpMessageDO.Article> articles;
}