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":"搜索", "name":"搜索",
"type":"view", "type":"view",
"url":"http://www.soso.com/" "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 = "公众号账号的编号不能为空") @NotNull(message = "公众号账号的编号不能为空")
private Long accountId; 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 lombok.*;
import io.swagger.annotations.*; import io.swagger.annotations.*;
import me.chanjar.weixin.common.bean.menu.WxMenuButton;
import javax.validation.constraints.NotNull;
import java.util.List;
@ApiModel("管理后台 - 微信菜单保存 Request VO") @ApiModel("管理后台 - 微信菜单保存 Request VO")
@Data @Data
@ -9,4 +13,7 @@ import io.swagger.annotations.*;
@ToString(callSuper = true) @ToString(callSuper = true)
public class MpMenuSaveReqVO extends MpMenuBaseVO { 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.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.*; import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.*;
import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
@ -16,4 +19,16 @@ public interface MpMenuConvert {
MpMenuRespVO convert(MpMenuDO bean); 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.responseMediaUrl", target = "mediaUrl"),
@Mapping(source = "reply.responseTitle", target = "title"), @Mapping(source = "reply.responseTitle", target = "title"),
@Mapping(source = "reply.responseDescription", target = "description"), @Mapping(source = "reply.responseDescription", target = "description"),
@Mapping(source = "reply.responseArticle", target = "article"), @Mapping(source = "reply.responseArticles", target = "articles"),
}) })
MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply); MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply);

View File

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

View File

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

View File

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

View File

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

View File

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