diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java new file mode 100644 index 000000000..a14c5fee1 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.mp.convert.message; + +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; +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; + +@Mapper +public interface MpAutoReplyConvert { + + MpAutoReplyConvert INSTANCE = Mappers.getMapper(MpAutoReplyConvert.class); + + @Mappings({ + @Mapping(source = "reply.appId", target = "appId"), + @Mapping(source = "reply.responseMessageType", target = "type"), + @Mapping(source = "reply.responseContent", target = "content"), + @Mapping(source = "reply.responseMediaId", target = "mediaId"), + @Mapping(source = "reply.responseMediaUrl", target = "mediaUrl"), + @Mapping(source = "reply.responseTitle", target = "title"), + @Mapping(source = "reply.responseDescription", target = "description"), + @Mapping(source = "reply.responseArticle", target = "article"), + }) + MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java index 9b7eae008..f440bf39b 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java @@ -1,20 +1,16 @@ package cn.iocoder.yudao.module.mp.convert.message; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageRespVO; import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; -import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage; import me.chanjar.weixin.mp.builder.outxml.BaseBuilder; -import me.chanjar.weixin.mp.builder.outxml.TextBuilder; -import me.chanjar.weixin.mp.builder.outxml.VideoBuilder; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; @@ -51,23 +47,27 @@ public interface MpMessageConvert { return message; } - default MpMessageDO convert(MpAutoReplyDO reply, MpAccountDO account, MpUserDO user) { + default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, MpAccountDO account, MpUserDO user) { // 构建消息 MpMessageDO message = new MpMessageDO(); - message.setType(reply.getResponseMessageType()); - // 1. 文本 - if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.TEXT)) { - message.setContent(reply.getResponseContent()); - } else if (ObjectUtils.equalsAny(reply.getResponseMessageType(), WxConsts.XmlMsgType.IMAGE, // 2. 图片 - WxConsts.XmlMsgType.VOICE)) { // 3. 语音 - message.setMediaId(reply.getResponseMediaId()).setMediaUrl(reply.getResponseMediaUrl()); - } else if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.VIDEO)) { // 4. 视频 - message.setMediaId(reply.getResponseMediaId()).setMediaUrl(reply.getResponseMediaUrl()) - .setTitle(reply.getResponseTitle()).setDescription(reply.getResponseDescription()); - } else if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.NEWS)) { // 5. 图文 - message.setArticles(Collections.singletonList(reply.getResponseArticle())); - } else { - throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); + message.setType(sendReqBO.getType()); + switch (sendReqBO.getType()) { + case WxConsts.XmlMsgType.TEXT: // 1. 文本 + message.setContent(sendReqBO.getContent()); + break; + case WxConsts.XmlMsgType.IMAGE: // 2. 图片 + case WxConsts.XmlMsgType.VOICE: // 3. 语音 + message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl()); + break; + case WxConsts.XmlMsgType.VIDEO: // 4. 视频 + message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl()) + .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()); + break; + case WxConsts.XmlMsgType.NEWS: // 5. 图文 + message.setArticles(Collections.singletonList(sendReqBO.getArticle())); + break; + default: + throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); } // 其它字段 diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java index 3d1c2bba4..24889116c 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java @@ -1,15 +1,19 @@ 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; +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.*; - -import com.baomidou.mybatisplus.annotation.*; -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import me.chanjar.weixin.common.bean.menu.WxMenu; -import me.chanjar.weixin.common.bean.menu.WxMenuButton; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.api.WxConsts.MenuButtonType; import java.util.List; @@ -48,8 +52,8 @@ public class MpMenuDO extends BaseDO { /** * 按钮列表 */ - @TableField(typeHandler = WxMenuButtonTypeHandler.class) - private List<WxMenuButton> buttons; + @TableField(typeHandler = ButtonTypeHandler.class) + private List<Button> buttons; /** * 同步状态 * @@ -58,16 +62,113 @@ public class MpMenuDO extends BaseDO { */ private Boolean syncStatus; + /** + * 按钮 + */ + @Data + public static class Button { + + /** + * 类型 + * + * 枚举 {@link MenuButtonType} + */ + private String type; + /** + * 消息类型 + * + * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG + * + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS + */ + private String messageType; + /** + * 名称 + */ + private String name; + /** + * 标识 + */ + private String key; + /** + * 二级菜单列表 + */ + private List<Button> subButtons; + /** + * 网页链接 + * + * 用户点击菜单可打开链接,不超过 1024 字节 + * + * 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM + */ + private String url; + + /** + * 小程序的 appId + * + * 类型为 {@link WxConsts.XmlMsgType} 的 MINIPROGRAM + */ + private String appId; + + /** + * 小程序的页面路径 + * + * 类型为 {@link WxConsts.XmlMsgType} 的 MINIPROGRAM + */ + private String pagePath; + + /** + * 消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + private String content; + + /** + * 媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String mediaId; + /** + * 媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String mediaUrl; + + /** + * 回复的标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String title; + /** + * 回复的描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String description; + + /** + * 图文消息 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + private MpMessageDO.Article article; + + } + // TODO @芋艿:可以找一些新的思路 - public static class WxMenuButtonTypeHandler extends AbstractJsonTypeHandler<List<WxMenuButton>> { + public static class ButtonTypeHandler extends AbstractJsonTypeHandler<List<Button>> { @Override - protected List<WxMenuButton> parse(String json) { - return JsonUtils.parseArray(json, WxMenuButton.class); + protected List<Button> parse(String json) { + return JsonUtils.parseArray(json, Button.class); } @Override - protected String toJson(List<WxMenuButton> obj) { + protected String toJson(List<Button> obj) { return JsonUtils.toJsonString(obj); } diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java index c07cc2eb6..1a9ed08c6 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java @@ -16,7 +16,6 @@ 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; /** diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java index 1aea757ba..d69b58f35 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java @@ -12,7 +12,9 @@ import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; import lombok.*; import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.mp.builder.kefu.NewsBuilder; +import javax.validation.constraints.NotEmpty; import java.io.Serializable; import java.util.List; @@ -199,20 +201,24 @@ public class MpMessageDO extends BaseDO { /** * 图文消息标题 */ + @NotEmpty(message = "图文消息标题不能为空", groups = NewsBuilder.class) private String title; /** * 图文消息描述 */ + @NotEmpty(message = "图文消息描述不能为空", groups = NewsBuilder.class) private String description; /** * 图片链接 * - * 支持JPG、PNG格式,较好的效果为大图 360*200,小图 200*200 + * 支持 JPG、PNG 格式,较好的效果为大图 360*200,小图 200*200 */ + @NotEmpty(message = "图片链接不能为空", groups = NewsBuilder.class) private String picUrl; /** * 点击图文消息跳转链接 */ + @NotEmpty(message = "点击图文消息跳转链接不能为空", groups = NewsBuilder.class) private String url; } diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java index 251aa8a53..d374e6a15 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java @@ -7,4 +7,8 @@ import org.apache.ibatis.annotations.Mapper; @Mapper public interface MpMenuMapper extends BaseMapperX<MpMenuDO> { + default MpMenuDO selectByAppId(String appId) { + return selectOne(MpMenuDO::getAppId, appId); + } + } diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java new file mode 100644 index 000000000..4a01b09ba --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.mp.framework.mp.core.util; + +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.api.WxConsts; + +import javax.validation.Validator; + +/** + * 公众号工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class MpUtils { + + /** + * Text 类型的消息,参数校验 Group + */ + public interface TextGroup {} + + /** + * Image 类型的消息,参数校验 Group + */ + public interface ImageGroup {} + + /** + * Voice 类型的消息,参数校验 Group + */ + public interface VoiceGroup {} + + /** + * Video 类型的消息,参数校验 Group + */ + public interface VideoGroup {} + + /** + * News 类型的消息,参数校验 Group + */ + public interface NewsGroup {} + + /** + * 校验消息的格式是否符合要求 + * + * @param type 类型 + * @param message 消息 + */ + public static void validateMessage(Validator validator, String type, Object message) { + // 获得对应的校验 group + Class<?> group; + switch (type) { + case WxConsts.XmlMsgType.TEXT: + group = TextGroup.class; + break; + case WxConsts.XmlMsgType.IMAGE: + group = ImageGroup.class; + break; + case WxConsts.XmlMsgType.VOICE: + group = VoiceGroup.class; + break; + case WxConsts.XmlMsgType.VIDEO: + group = VideoGroup.class; + break; + case WxConsts.XmlMsgType.NEWS: + group = NewsGroup.class; + break; + default: + log.error("[validateMessage][未知的消息类型({})]", message); + throw new IllegalArgumentException("不支持的消息类型:" + type); + } + // 执行校验 + ValidationUtils.validate(validator, message, group); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/menu/MenuHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/menu/MenuHandler.java index f04efedcb..24d471fbf 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/menu/MenuHandler.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/menu/MenuHandler.java @@ -1,12 +1,16 @@ package cn.iocoder.yudao.module.mp.service.handler.menu; +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; +import cn.iocoder.yudao.module.mp.service.menu.MpMenuService; import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMenuService; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.stereotype.Component; +import javax.annotation.Resource; import java.util.Map; import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType; @@ -14,24 +18,20 @@ import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType; /** * 自定义菜单的事件处理器 * - * // TODO 芋艿:待实现 + * 逻辑:用户点击菜单时,触发对应的回复 + * + * @author 芋道源码 */ @Component public class MenuHandler implements WxMpMessageHandler { + @Resource + private MpMenuService mpMenuService; + @Override - public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, - Map<String, Object> context, WxMpService weixinService, - WxSessionManager sessionManager) { - - String msg = wxMessage.getEventKey(); - if (MenuButtonType.VIEW.equals(wxMessage.getEvent())) { - return null; - } - - return WxMpXmlOutMessage.TEXT().content(msg) - .fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser()) - .build(); + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, + WxMpService weixinService, WxSessionManager sessionManager) { + return mpMenuService.reply(MpContextHolder.getAppId(), wxMessage.getEventKey(), wxMessage.getFromUser()); } } diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java index ef5fde52e..4e4a4ebf4 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java @@ -29,7 +29,7 @@ public class MessageReceiveHandler implements WxMpMessageHandler { public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) { log.info("[handle][接收到请求消息,内容:{}]", wxMessage); - mpMessageService.createFromUser(MpContextHolder.getAppId(), wxMessage); + mpMessageService.receiveMessage(MpContextHolder.getAppId(), wxMessage); return null; } diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuService.java index 1bb45a381..439783951 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuService.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuService.java @@ -4,6 +4,8 @@ import javax.validation.*; import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.*; import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; /** * 微信菜单 Service 接口 @@ -35,4 +37,14 @@ public interface MpMenuService { */ MpMenuDO getMenu(Long id); + /** + * 用户点击菜单按钮时,回复对应的消息 + * + * @param appId 公众号 AppId + * @param key 菜单按钮的标识 + * @param openid 用户的 openid + * @return 消息 + */ + WxMpXmlOutMessage reply(String appId, String key, String openid); + } diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java index 823a1dad7..f2728d5f5 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java @@ -1,11 +1,18 @@ 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 lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.bean.menu.WxMenu; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -27,6 +34,7 @@ import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*; */ @Service @Validated +@Slf4j public class MpMenuServiceImpl implements MpMenuService { @Resource @@ -77,4 +85,49 @@ public class MpMenuServiceImpl implements MpMenuService { return mpMenuMapper.selectById(id); } + @Override + public WxMpXmlOutMessage reply(String appId, String key, String openid) { + // 获得菜单 + MpMenuDO menu = mpMenuMapper.selectByAppId(appId); + 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); + return null; + } + // 按钮必须要有消息类型,不然后续无法回复消息 + if (StrUtil.isEmpty(button.getMessageType())) { + log.error("[reply][appId({}) key({}) 不存在消息类型({})]", appId, key, button); + 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; + } + } diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyServiceImpl.java index 8d4f63d63..d5f323aef 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyServiceImpl.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyServiceImpl.java @@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.mp.service.message; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.mp.convert.message.MpAutoReplyConvert; import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; import cn.iocoder.yudao.module.mp.dal.mysql.message.MpAutoReplyMapper; import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum; import cn.iocoder.yudao.module.mp.service.account.MpAccountService; +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; @@ -58,7 +60,8 @@ public class MpAutoReplyServiceImpl implements MpAutoReplyService { MpAutoReplyDO reply = CollUtil.getFirst(replies); // 第二步,基于自动回复,创建消息 - return mpMessageService.createFromAutoReply(wxMessage.getFromUser(), reply); + MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); + return mpMessageService.sendOutMessage(sendReqBO); } @Override @@ -69,7 +72,8 @@ public class MpAutoReplyServiceImpl implements MpAutoReplyService { : buildDefaultSubscribeAutoReply(appId); // 如果不存在,提供一个默认末班 // 第二步,基于自动回复,创建消息 - return mpMessageService.createFromAutoReply(wxMessage.getFromUser(), reply); + MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); + return mpMessageService.sendOutMessage(sendReqBO); } private MpAutoReplyDO buildDefaultSubscribeAutoReply(String appId) { diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java index 4270cbe8a..8082f7e60 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java @@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.mp.service.message; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; -import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import javax.validation.Valid; + /** * 粉丝消息表 Service 接口 * @@ -24,20 +26,23 @@ public interface MpMessageService { PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO); /** - * 保存粉丝消息,来自用户发送 + * 从公众号,接收到用户消息 * * @param appId 微信公众号 appId * @param wxMessage 消息 */ - void createFromUser(String appId, WxMpXmlMessage wxMessage); + void receiveMessage(String appId, WxMpXmlMessage wxMessage); /** - * 创建粉丝消息,通过自动回复 + * 使用公众号,给用户回复消息 * - * @param openid 公众号粉丝 openid - * @param reply 自动回复 + * 例如说:自动回复、客服消息、菜单回复消息等场景 + * + * 注意,该方法只是返回 WxMpXmlOutMessage 对象,不会真的发送消息 + * + * @param sendReqBO 消息内容 * @return 微信回复消息 XML */ - WxMpXmlOutMessage createFromAutoReply(String openid, MpAutoReplyDO reply); + WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO); } diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java index dbf52ebe9..ee7b8c46c 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java @@ -2,21 +2,22 @@ package cn.iocoder.yudao.module.mp.service.message; import cn.hutool.core.io.FileTypeUtil; import cn.hutool.core.io.FileUtil; -import cn.hutool.core.io.IoUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.util.io.FileUtils; +import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert; import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; -import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; +import cn.iocoder.yudao.module.mp.dal.mysql.message.MpMessageMapper; import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum; import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; import cn.iocoder.yudao.module.mp.service.account.MpAccountService; +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; import cn.iocoder.yudao.module.mp.service.user.MpUserService; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; @@ -24,20 +25,12 @@ import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; - -import javax.annotation.Resource; - import org.springframework.validation.annotation.Validated; -import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*; -import cn.iocoder.yudao.framework.common.pojo.PageResult; - -import cn.iocoder.yudao.module.mp.dal.mysql.message.MpMessageMapper; - +import javax.annotation.Resource; +import javax.validation.Validator; import java.io.File; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; - /** * 粉丝消息表 Service 实现类 * @@ -64,13 +57,16 @@ public class MpMessageServiceImpl implements MpMessageService { @Resource private FileApi fileApi; + @Resource + private Validator validator; + @Override public PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO) { return mpMessageMapper.selectPage(pageReqVO); } @Override - public void createFromUser(String appId, WxMpXmlMessage wxMessage) { + public void receiveMessage(String appId, WxMpXmlMessage wxMessage) { WxMpService mpService = mpServiceFactory.getRequiredMpService(appId); // 获得关联信息 MpAccountDO account = mpAccountService.getAccountFromCache(appId); @@ -88,24 +84,21 @@ public class MpMessageServiceImpl implements MpMessageService { message.setThumbMediaUrl(mediaDownload(mpService, message.getThumbMediaId())); } mpMessageMapper.insert(message); - -// WxConsts.MenuButtonType.VIEW TODO 芋艿:待测试 -// wxMessage.getEventKey() - -// WxConsts.MenuButtonType.CLICK -// wxMessage.getEventKey() } @Override - public WxMpXmlOutMessage createFromAutoReply(String openid, MpAutoReplyDO reply) { + public WxMpXmlOutMessage sendOutMessage(MpMessageSendOutReqBO sendReqBO) { + // 校验消息格式 + MpUtils.validateMessage(validator, sendReqBO.getType(), sendReqBO); + // 获得关联信息 - MpAccountDO account = mpAccountService.getAccountFromCache(reply.getAppId()); - Assert.notNull(account, "公众号账号({}) 不存在", reply.getAppId()); - MpUserDO user = mpUserService.getUser(reply.getAppId(), openid); - Assert.notNull(user, "公众号粉丝({}/{}) 不存在", reply.getAppId(), openid); + MpAccountDO account = mpAccountService.getAccountFromCache(sendReqBO.getAppId()); + Assert.notNull(account, "公众号账号({}) 不存在", sendReqBO.getAppId()); + MpUserDO user = mpUserService.getUser(sendReqBO.getAppId(), sendReqBO.getOpenid()); + Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid()); // 记录消息 - MpMessageDO message = MpMessageConvert.INSTANCE.convert(reply, account, user); + MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user); message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); mpMessageMapper.insert(message); diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java new file mode 100644 index 000000000..3c5eeb97e --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.mp.service.message.bo; + +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 公众号消息发送 Request BO + * + * 为什么要有该 BO 呢?在自动回复、客服消息、菜单回复消息等场景,都涉及到 MP 给用户发送消息,所以使用该 BO 统一承接 + * + * @author 芋道源码 + */ +@Data +public class MpMessageSendOutReqBO { + + /** + * 公众号 appId + */ + @NotEmpty(message = "公众号 appId 不能为空") + private String appId; + /** + * 公众号用户 openid + */ + @NotEmpty(message = "公众号用户 openid 不能为空") + private String openid; + + // ========== 消息内容 ========== + /** + * 消息类型 + * + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS + */ + @NotEmpty(message = "消息类型不能为空") + public String type; + + /** + * 消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + @NotEmpty(message = "消息内容不能为空", groups = TextGroup.class) + private String content; + + /** + * 媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) + private String mediaId; + /** + * 媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) + private String mediaUrl; + + /** + * 标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) + private String title; + /** + * 描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) + private String description; + + /** + * 图文消息 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @Valid + @NotNull(message = "图文消息不能为空", groups = NewsGroup.class) + private MpMessageDO.Article article; + +}