Commit 665f4b2b0926f4cf10b295724ffa06be04064a92
1 parent
47f07156
mp:重构 message 发送消息的方法
Showing
15 changed files
with
453 additions
and
84 deletions
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.convert.message; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; | |
| 4 | +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; | |
| 5 | +import org.mapstruct.Mapper; | |
| 6 | +import org.mapstruct.Mapping; | |
| 7 | +import org.mapstruct.Mappings; | |
| 8 | +import org.mapstruct.factory.Mappers; | |
| 9 | + | |
| 10 | +@Mapper | |
| 11 | +public interface MpAutoReplyConvert { | |
| 12 | + | |
| 13 | + MpAutoReplyConvert INSTANCE = Mappers.getMapper(MpAutoReplyConvert.class); | |
| 14 | + | |
| 15 | + @Mappings({ | |
| 16 | + @Mapping(source = "reply.appId", target = "appId"), | |
| 17 | + @Mapping(source = "reply.responseMessageType", target = "type"), | |
| 18 | + @Mapping(source = "reply.responseContent", target = "content"), | |
| 19 | + @Mapping(source = "reply.responseMediaId", target = "mediaId"), | |
| 20 | + @Mapping(source = "reply.responseMediaUrl", target = "mediaUrl"), | |
| 21 | + @Mapping(source = "reply.responseTitle", target = "title"), | |
| 22 | + @Mapping(source = "reply.responseDescription", target = "description"), | |
| 23 | + @Mapping(source = "reply.responseArticle", target = "article"), | |
| 24 | + }) | |
| 25 | + MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply); | |
| 26 | + | |
| 27 | +} | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java
| 1 | 1 | package cn.iocoder.yudao.module.mp.convert.message; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.util.StrUtil; | |
| 4 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; | |
| 6 | 4 | import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageRespVO; |
| 7 | 5 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 8 | -import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; | |
| 9 | 6 | import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; |
| 10 | 7 | import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; |
| 8 | +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; | |
| 11 | 9 | import me.chanjar.weixin.common.api.WxConsts; |
| 12 | 10 | import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; |
| 13 | 11 | import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; |
| 14 | 12 | import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage; |
| 15 | 13 | import me.chanjar.weixin.mp.builder.outxml.BaseBuilder; |
| 16 | -import me.chanjar.weixin.mp.builder.outxml.TextBuilder; | |
| 17 | -import me.chanjar.weixin.mp.builder.outxml.VideoBuilder; | |
| 18 | 14 | import org.mapstruct.Mapper; |
| 19 | 15 | import org.mapstruct.Mapping; |
| 20 | 16 | import org.mapstruct.Mappings; |
| ... | ... | @@ -51,23 +47,27 @@ public interface MpMessageConvert { |
| 51 | 47 | return message; |
| 52 | 48 | } |
| 53 | 49 | |
| 54 | - default MpMessageDO convert(MpAutoReplyDO reply, MpAccountDO account, MpUserDO user) { | |
| 50 | + default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, MpAccountDO account, MpUserDO user) { | |
| 55 | 51 | // 构建消息 |
| 56 | 52 | MpMessageDO message = new MpMessageDO(); |
| 57 | - message.setType(reply.getResponseMessageType()); | |
| 58 | - // 1. 文本 | |
| 59 | - if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.TEXT)) { | |
| 60 | - message.setContent(reply.getResponseContent()); | |
| 61 | - } else if (ObjectUtils.equalsAny(reply.getResponseMessageType(), WxConsts.XmlMsgType.IMAGE, // 2. 图片 | |
| 62 | - WxConsts.XmlMsgType.VOICE)) { // 3. 语音 | |
| 63 | - message.setMediaId(reply.getResponseMediaId()).setMediaUrl(reply.getResponseMediaUrl()); | |
| 64 | - } else if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.VIDEO)) { // 4. 视频 | |
| 65 | - message.setMediaId(reply.getResponseMediaId()).setMediaUrl(reply.getResponseMediaUrl()) | |
| 66 | - .setTitle(reply.getResponseTitle()).setDescription(reply.getResponseDescription()); | |
| 67 | - } else if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.NEWS)) { // 5. 图文 | |
| 68 | - message.setArticles(Collections.singletonList(reply.getResponseArticle())); | |
| 69 | - } else { | |
| 70 | - throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); | |
| 53 | + message.setType(sendReqBO.getType()); | |
| 54 | + switch (sendReqBO.getType()) { | |
| 55 | + case WxConsts.XmlMsgType.TEXT: // 1. 文本 | |
| 56 | + message.setContent(sendReqBO.getContent()); | |
| 57 | + break; | |
| 58 | + case WxConsts.XmlMsgType.IMAGE: // 2. 图片 | |
| 59 | + case WxConsts.XmlMsgType.VOICE: // 3. 语音 | |
| 60 | + message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl()); | |
| 61 | + break; | |
| 62 | + case WxConsts.XmlMsgType.VIDEO: // 4. 视频 | |
| 63 | + message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl()) | |
| 64 | + .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()); | |
| 65 | + break; | |
| 66 | + case WxConsts.XmlMsgType.NEWS: // 5. 图文 | |
| 67 | + message.setArticles(Collections.singletonList(sendReqBO.getArticle())); | |
| 68 | + break; | |
| 69 | + default: | |
| 70 | + throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); | |
| 71 | 71 | } |
| 72 | 72 | |
| 73 | 73 | // 其它字段 | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/menu/MpMenuDO.java
| 1 | 1 | package cn.iocoder.yudao.module.mp.dal.dataobject.menu; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | |
| 4 | 5 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 5 | 6 | import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; |
| 7 | +import com.baomidou.mybatisplus.annotation.KeySequence; | |
| 8 | +import com.baomidou.mybatisplus.annotation.TableField; | |
| 9 | +import com.baomidou.mybatisplus.annotation.TableId; | |
| 10 | +import com.baomidou.mybatisplus.annotation.TableName; | |
| 6 | 11 | import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; |
| 7 | -import lombok.*; | |
| 8 | - | |
| 9 | -import com.baomidou.mybatisplus.annotation.*; | |
| 10 | -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | |
| 11 | -import me.chanjar.weixin.common.bean.menu.WxMenu; | |
| 12 | -import me.chanjar.weixin.common.bean.menu.WxMenuButton; | |
| 12 | +import lombok.Data; | |
| 13 | +import lombok.EqualsAndHashCode; | |
| 14 | +import lombok.ToString; | |
| 15 | +import me.chanjar.weixin.common.api.WxConsts; | |
| 16 | +import me.chanjar.weixin.common.api.WxConsts.MenuButtonType; | |
| 13 | 17 | |
| 14 | 18 | import java.util.List; |
| 15 | 19 | |
| ... | ... | @@ -48,8 +52,8 @@ public class MpMenuDO extends BaseDO { |
| 48 | 52 | /** |
| 49 | 53 | * 按钮列表 |
| 50 | 54 | */ |
| 51 | - @TableField(typeHandler = WxMenuButtonTypeHandler.class) | |
| 52 | - private List<WxMenuButton> buttons; | |
| 55 | + @TableField(typeHandler = ButtonTypeHandler.class) | |
| 56 | + private List<Button> buttons; | |
| 53 | 57 | /** |
| 54 | 58 | * 同步状态 |
| 55 | 59 | * |
| ... | ... | @@ -58,16 +62,113 @@ public class MpMenuDO extends BaseDO { |
| 58 | 62 | */ |
| 59 | 63 | private Boolean syncStatus; |
| 60 | 64 | |
| 65 | + /** | |
| 66 | + * 按钮 | |
| 67 | + */ | |
| 68 | + @Data | |
| 69 | + public static class Button { | |
| 70 | + | |
| 71 | + /** | |
| 72 | + * 类型 | |
| 73 | + * | |
| 74 | + * 枚举 {@link MenuButtonType} | |
| 75 | + */ | |
| 76 | + private String type; | |
| 77 | + /** | |
| 78 | + * 消息类型 | |
| 79 | + * | |
| 80 | + * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG | |
| 81 | + * | |
| 82 | + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS | |
| 83 | + */ | |
| 84 | + private String messageType; | |
| 85 | + /** | |
| 86 | + * 名称 | |
| 87 | + */ | |
| 88 | + private String name; | |
| 89 | + /** | |
| 90 | + * 标识 | |
| 91 | + */ | |
| 92 | + private String key; | |
| 93 | + /** | |
| 94 | + * 二级菜单列表 | |
| 95 | + */ | |
| 96 | + private List<Button> subButtons; | |
| 97 | + /** | |
| 98 | + * 网页链接 | |
| 99 | + * | |
| 100 | + * 用户点击菜单可打开链接,不超过 1024 字节 | |
| 101 | + * | |
| 102 | + * 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM | |
| 103 | + */ | |
| 104 | + private String url; | |
| 105 | + | |
| 106 | + /** | |
| 107 | + * 小程序的 appId | |
| 108 | + * | |
| 109 | + * 类型为 {@link WxConsts.XmlMsgType} 的 MINIPROGRAM | |
| 110 | + */ | |
| 111 | + private String appId; | |
| 112 | + | |
| 113 | + /** | |
| 114 | + * 小程序的页面路径 | |
| 115 | + * | |
| 116 | + * 类型为 {@link WxConsts.XmlMsgType} 的 MINIPROGRAM | |
| 117 | + */ | |
| 118 | + private String pagePath; | |
| 119 | + | |
| 120 | + /** | |
| 121 | + * 消息内容 | |
| 122 | + * | |
| 123 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT | |
| 124 | + */ | |
| 125 | + private String content; | |
| 126 | + | |
| 127 | + /** | |
| 128 | + * 媒体 id | |
| 129 | + * | |
| 130 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO | |
| 131 | + */ | |
| 132 | + private String mediaId; | |
| 133 | + /** | |
| 134 | + * 媒体 URL | |
| 135 | + * | |
| 136 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO | |
| 137 | + */ | |
| 138 | + private String mediaUrl; | |
| 139 | + | |
| 140 | + /** | |
| 141 | + * 回复的标题 | |
| 142 | + * | |
| 143 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO | |
| 144 | + */ | |
| 145 | + private String title; | |
| 146 | + /** | |
| 147 | + * 回复的描述 | |
| 148 | + * | |
| 149 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO | |
| 150 | + */ | |
| 151 | + private String description; | |
| 152 | + | |
| 153 | + /** | |
| 154 | + * 图文消息 | |
| 155 | + * | |
| 156 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS | |
| 157 | + */ | |
| 158 | + private MpMessageDO.Article article; | |
| 159 | + | |
| 160 | + } | |
| 161 | + | |
| 61 | 162 | // TODO @芋艿:可以找一些新的思路 |
| 62 | - public static class WxMenuButtonTypeHandler extends AbstractJsonTypeHandler<List<WxMenuButton>> { | |
| 163 | + public static class ButtonTypeHandler extends AbstractJsonTypeHandler<List<Button>> { | |
| 63 | 164 | |
| 64 | 165 | @Override |
| 65 | - protected List<WxMenuButton> parse(String json) { | |
| 66 | - return JsonUtils.parseArray(json, WxMenuButton.class); | |
| 166 | + protected List<Button> parse(String json) { | |
| 167 | + return JsonUtils.parseArray(json, Button.class); | |
| 67 | 168 | } |
| 68 | 169 | |
| 69 | 170 | @Override |
| 70 | - protected String toJson(List<WxMenuButton> obj) { | |
| 171 | + protected String toJson(List<Button> obj) { | |
| 71 | 172 | return JsonUtils.toJsonString(obj); |
| 72 | 173 | } |
| 73 | 174 | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java
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; |
| 12 | 12 | import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; |
| 13 | 13 | import lombok.*; |
| 14 | 14 | import me.chanjar.weixin.common.api.WxConsts; |
| 15 | +import me.chanjar.weixin.mp.builder.kefu.NewsBuilder; | |
| 15 | 16 | |
| 17 | +import javax.validation.constraints.NotEmpty; | |
| 16 | 18 | import java.io.Serializable; |
| 17 | 19 | import java.util.List; |
| 18 | 20 | |
| ... | ... | @@ -199,20 +201,24 @@ public class MpMessageDO extends BaseDO { |
| 199 | 201 | /** |
| 200 | 202 | * 图文消息标题 |
| 201 | 203 | */ |
| 204 | + @NotEmpty(message = "图文消息标题不能为空", groups = NewsBuilder.class) | |
| 202 | 205 | private String title; |
| 203 | 206 | /** |
| 204 | 207 | * 图文消息描述 |
| 205 | 208 | */ |
| 209 | + @NotEmpty(message = "图文消息描述不能为空", groups = NewsBuilder.class) | |
| 206 | 210 | private String description; |
| 207 | 211 | /** |
| 208 | 212 | * 图片链接 |
| 209 | 213 | * |
| 210 | - * 支持JPG、PNG格式,较好的效果为大图 360*200,小图 200*200 | |
| 214 | + * 支持 JPG、PNG 格式,较好的效果为大图 360*200,小图 200*200 | |
| 211 | 215 | */ |
| 216 | + @NotEmpty(message = "图片链接不能为空", groups = NewsBuilder.class) | |
| 212 | 217 | private String picUrl; |
| 213 | 218 | /** |
| 214 | 219 | * 点击图文消息跳转链接 |
| 215 | 220 | */ |
| 221 | + @NotEmpty(message = "点击图文消息跳转链接不能为空", groups = NewsBuilder.class) | |
| 216 | 222 | private String url; |
| 217 | 223 | |
| 218 | 224 | } | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.framework.mp.core.util; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; | |
| 4 | +import lombok.extern.slf4j.Slf4j; | |
| 5 | +import me.chanjar.weixin.common.api.WxConsts; | |
| 6 | + | |
| 7 | +import javax.validation.Validator; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * 公众号工具类 | |
| 11 | + * | |
| 12 | + * @author 芋道源码 | |
| 13 | + */ | |
| 14 | +@Slf4j | |
| 15 | +public class MpUtils { | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * Text 类型的消息,参数校验 Group | |
| 19 | + */ | |
| 20 | + public interface TextGroup {} | |
| 21 | + | |
| 22 | + /** | |
| 23 | + * Image 类型的消息,参数校验 Group | |
| 24 | + */ | |
| 25 | + public interface ImageGroup {} | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * Voice 类型的消息,参数校验 Group | |
| 29 | + */ | |
| 30 | + public interface VoiceGroup {} | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * Video 类型的消息,参数校验 Group | |
| 34 | + */ | |
| 35 | + public interface VideoGroup {} | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * News 类型的消息,参数校验 Group | |
| 39 | + */ | |
| 40 | + public interface NewsGroup {} | |
| 41 | + | |
| 42 | + /** | |
| 43 | + * 校验消息的格式是否符合要求 | |
| 44 | + * | |
| 45 | + * @param type 类型 | |
| 46 | + * @param message 消息 | |
| 47 | + */ | |
| 48 | + public static void validateMessage(Validator validator, String type, Object message) { | |
| 49 | + // 获得对应的校验 group | |
| 50 | + Class<?> group; | |
| 51 | + switch (type) { | |
| 52 | + case WxConsts.XmlMsgType.TEXT: | |
| 53 | + group = TextGroup.class; | |
| 54 | + break; | |
| 55 | + case WxConsts.XmlMsgType.IMAGE: | |
| 56 | + group = ImageGroup.class; | |
| 57 | + break; | |
| 58 | + case WxConsts.XmlMsgType.VOICE: | |
| 59 | + group = VoiceGroup.class; | |
| 60 | + break; | |
| 61 | + case WxConsts.XmlMsgType.VIDEO: | |
| 62 | + group = VideoGroup.class; | |
| 63 | + break; | |
| 64 | + case WxConsts.XmlMsgType.NEWS: | |
| 65 | + group = NewsGroup.class; | |
| 66 | + break; | |
| 67 | + default: | |
| 68 | + log.error("[validateMessage][未知的消息类型({})]", message); | |
| 69 | + throw new IllegalArgumentException("不支持的消息类型:" + type); | |
| 70 | + } | |
| 71 | + // 执行校验 | |
| 72 | + ValidationUtils.validate(validator, message, group); | |
| 73 | + } | |
| 74 | + | |
| 75 | +} | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/menu/MenuHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.mp.service.handler.menu; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; | |
| 4 | +import cn.iocoder.yudao.module.mp.service.menu.MpMenuService; | |
| 3 | 5 | import me.chanjar.weixin.common.session.WxSessionManager; |
| 6 | +import me.chanjar.weixin.mp.api.WxMpMenuService; | |
| 4 | 7 | import me.chanjar.weixin.mp.api.WxMpMessageHandler; |
| 5 | 8 | import me.chanjar.weixin.mp.api.WxMpService; |
| 6 | 9 | import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; |
| 7 | 10 | import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; |
| 8 | 11 | import org.springframework.stereotype.Component; |
| 9 | 12 | |
| 13 | +import javax.annotation.Resource; | |
| 10 | 14 | import java.util.Map; |
| 11 | 15 | |
| 12 | 16 | import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType; |
| ... | ... | @@ -14,24 +18,20 @@ import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType; |
| 14 | 18 | /** |
| 15 | 19 | * 自定义菜单的事件处理器 |
| 16 | 20 | * |
| 17 | - * // TODO 芋艿:待实现 | |
| 21 | + * 逻辑:用户点击菜单时,触发对应的回复 | |
| 22 | + * | |
| 23 | + * @author 芋道源码 | |
| 18 | 24 | */ |
| 19 | 25 | @Component |
| 20 | 26 | public class MenuHandler implements WxMpMessageHandler { |
| 21 | 27 | |
| 22 | - @Override | |
| 23 | - public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, | |
| 24 | - Map<String, Object> context, WxMpService weixinService, | |
| 25 | - WxSessionManager sessionManager) { | |
| 26 | - | |
| 27 | - String msg = wxMessage.getEventKey(); | |
| 28 | - if (MenuButtonType.VIEW.equals(wxMessage.getEvent())) { | |
| 29 | - return null; | |
| 30 | - } | |
| 28 | + @Resource | |
| 29 | + private MpMenuService mpMenuService; | |
| 31 | 30 | |
| 32 | - return WxMpXmlOutMessage.TEXT().content(msg) | |
| 33 | - .fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser()) | |
| 34 | - .build(); | |
| 31 | + @Override | |
| 32 | + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, | |
| 33 | + WxMpService weixinService, WxSessionManager sessionManager) { | |
| 34 | + return mpMenuService.reply(MpContextHolder.getAppId(), wxMessage.getEventKey(), wxMessage.getFromUser()); | |
| 35 | 35 | } |
| 36 | 36 | |
| 37 | 37 | } | ... | ... |
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 { |
| 29 | 29 | public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, |
| 30 | 30 | WxMpService wxMpService, WxSessionManager sessionManager) { |
| 31 | 31 | log.info("[handle][接收到请求消息,内容:{}]", wxMessage); |
| 32 | - mpMessageService.createFromUser(MpContextHolder.getAppId(), wxMessage); | |
| 32 | + mpMessageService.receiveMessage(MpContextHolder.getAppId(), wxMessage); | |
| 33 | 33 | return null; |
| 34 | 34 | } |
| 35 | 35 | ... | ... |
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.*; |
| 4 | 4 | |
| 5 | 5 | import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.*; |
| 6 | 6 | import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; |
| 7 | +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; | |
| 8 | +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; | |
| 7 | 9 | |
| 8 | 10 | /** |
| 9 | 11 | * 微信菜单 Service 接口 |
| ... | ... | @@ -35,4 +37,14 @@ public interface MpMenuService { |
| 35 | 37 | */ |
| 36 | 38 | MpMenuDO getMenu(Long id); |
| 37 | 39 | |
| 40 | + /** | |
| 41 | + * 用户点击菜单按钮时,回复对应的消息 | |
| 42 | + * | |
| 43 | + * @param appId 公众号 AppId | |
| 44 | + * @param key 菜单按钮的标识 | |
| 45 | + * @param openid 用户的 openid | |
| 46 | + * @return 消息 | |
| 47 | + */ | |
| 48 | + WxMpXmlOutMessage reply(String appId, String key, String openid); | |
| 49 | + | |
| 38 | 50 | } | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.mp.service.menu; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.lang.Assert; | |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 6 | import cn.iocoder.yudao.module.mp.convert.menu.MpMenuConvert; |
| 7 | +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; | |
| 4 | 8 | import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; |
| 5 | 9 | import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; |
| 10 | +import cn.iocoder.yudao.module.mp.service.account.MpAccountService; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 6 | 12 | import me.chanjar.weixin.common.bean.menu.WxMenu; |
| 7 | 13 | import me.chanjar.weixin.common.error.WxErrorException; |
| 8 | 14 | import me.chanjar.weixin.mp.api.WxMpService; |
| 15 | +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; | |
| 9 | 16 | import org.springframework.context.annotation.Lazy; |
| 10 | 17 | import org.springframework.stereotype.Service; |
| 11 | 18 | |
| ... | ... | @@ -27,6 +34,7 @@ import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*; |
| 27 | 34 | */ |
| 28 | 35 | @Service |
| 29 | 36 | @Validated |
| 37 | +@Slf4j | |
| 30 | 38 | public class MpMenuServiceImpl implements MpMenuService { |
| 31 | 39 | |
| 32 | 40 | @Resource |
| ... | ... | @@ -77,4 +85,49 @@ public class MpMenuServiceImpl implements MpMenuService { |
| 77 | 85 | return mpMenuMapper.selectById(id); |
| 78 | 86 | } |
| 79 | 87 | |
| 88 | + @Override | |
| 89 | + public WxMpXmlOutMessage reply(String appId, String key, String openid) { | |
| 90 | + // 获得菜单 | |
| 91 | + MpMenuDO menu = mpMenuMapper.selectByAppId(appId); | |
| 92 | + if (menu == null) { | |
| 93 | + log.error("[reply][appId({}) 找不到对应的菜单]", appId); | |
| 94 | + return null; | |
| 95 | + } | |
| 96 | + // 匹配对应的按钮 | |
| 97 | + MpMenuDO.Button button = getMenuButton(menu, key); | |
| 98 | + if (button == null) { | |
| 99 | + log.error("[reply][appId({}) key({}) 找不到对应的菜单按钮]", appId, key); | |
| 100 | + return null; | |
| 101 | + } | |
| 102 | + // 按钮必须要有消息类型,不然后续无法回复消息 | |
| 103 | + if (StrUtil.isEmpty(button.getMessageType())) { | |
| 104 | + log.error("[reply][appId({}) key({}) 不存在消息类型({})]", appId, key, button); | |
| 105 | + return null; | |
| 106 | + } | |
| 107 | + | |
| 108 | + // 回复消息 | |
| 109 | + return null; | |
| 110 | + } | |
| 111 | + | |
| 112 | + private MpMenuDO.Button getMenuButton(MpMenuDO menu, String key) { | |
| 113 | + // 先查询子按钮 | |
| 114 | + for (MpMenuDO.Button button : menu.getButtons()) { | |
| 115 | + if (CollUtil.isEmpty(button.getSubButtons())) { | |
| 116 | + continue; | |
| 117 | + } | |
| 118 | + for (MpMenuDO.Button subButton : button.getSubButtons()) { | |
| 119 | + if (StrUtil.equals(subButton.getKey(), key)) { | |
| 120 | + return subButton; | |
| 121 | + } | |
| 122 | + } | |
| 123 | + } | |
| 124 | + // 再查询父按钮 | |
| 125 | + for (MpMenuDO.Button button : menu.getButtons()) { | |
| 126 | + if (StrUtil.equals(button.getKey(), key)) { | |
| 127 | + return button; | |
| 128 | + } | |
| 129 | + } | |
| 130 | + return null; | |
| 131 | + } | |
| 132 | + | |
| 80 | 133 | } | ... | ... |
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; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.lang.Assert; |
| 5 | +import cn.iocoder.yudao.module.mp.convert.message.MpAutoReplyConvert; | |
| 5 | 6 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 6 | 7 | import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; |
| 7 | 8 | import cn.iocoder.yudao.module.mp.dal.mysql.message.MpAutoReplyMapper; |
| 8 | 9 | import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum; |
| 9 | 10 | import cn.iocoder.yudao.module.mp.service.account.MpAccountService; |
| 11 | +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; | |
| 10 | 12 | import me.chanjar.weixin.common.api.WxConsts; |
| 11 | 13 | import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; |
| 12 | 14 | import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; |
| ... | ... | @@ -58,7 +60,8 @@ public class MpAutoReplyServiceImpl implements MpAutoReplyService { |
| 58 | 60 | MpAutoReplyDO reply = CollUtil.getFirst(replies); |
| 59 | 61 | |
| 60 | 62 | // 第二步,基于自动回复,创建消息 |
| 61 | - return mpMessageService.createFromAutoReply(wxMessage.getFromUser(), reply); | |
| 63 | + MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); | |
| 64 | + return mpMessageService.sendOutMessage(sendReqBO); | |
| 62 | 65 | } |
| 63 | 66 | |
| 64 | 67 | @Override |
| ... | ... | @@ -69,7 +72,8 @@ public class MpAutoReplyServiceImpl implements MpAutoReplyService { |
| 69 | 72 | : buildDefaultSubscribeAutoReply(appId); // 如果不存在,提供一个默认末班 |
| 70 | 73 | |
| 71 | 74 | // 第二步,基于自动回复,创建消息 |
| 72 | - return mpMessageService.createFromAutoReply(wxMessage.getFromUser(), reply); | |
| 75 | + MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); | |
| 76 | + return mpMessageService.sendOutMessage(sendReqBO); | |
| 73 | 77 | } |
| 74 | 78 | |
| 75 | 79 | private MpAutoReplyDO buildDefaultSubscribeAutoReply(String appId) { | ... | ... |
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; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; |
| 5 | -import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; | |
| 6 | 5 | import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; |
| 6 | +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; | |
| 7 | 7 | import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; |
| 8 | 8 | import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; |
| 9 | 9 | |
| 10 | +import javax.validation.Valid; | |
| 11 | + | |
| 10 | 12 | /** |
| 11 | 13 | * 粉丝消息表 Service 接口 |
| 12 | 14 | * |
| ... | ... | @@ -24,20 +26,23 @@ public interface MpMessageService { |
| 24 | 26 | PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO); |
| 25 | 27 | |
| 26 | 28 | /** |
| 27 | - * 保存粉丝消息,来自用户发送 | |
| 29 | + * 从公众号,接收到用户消息 | |
| 28 | 30 | * |
| 29 | 31 | * @param appId 微信公众号 appId |
| 30 | 32 | * @param wxMessage 消息 |
| 31 | 33 | */ |
| 32 | - void createFromUser(String appId, WxMpXmlMessage wxMessage); | |
| 34 | + void receiveMessage(String appId, WxMpXmlMessage wxMessage); | |
| 33 | 35 | |
| 34 | 36 | /** |
| 35 | - * 创建粉丝消息,通过自动回复 | |
| 37 | + * 使用公众号,给用户回复消息 | |
| 38 | + * | |
| 39 | + * 例如说:自动回复、客服消息、菜单回复消息等场景 | |
| 40 | + * | |
| 41 | + * 注意,该方法只是返回 WxMpXmlOutMessage 对象,不会真的发送消息 | |
| 36 | 42 | * |
| 37 | - * @param openid 公众号粉丝 openid | |
| 38 | - * @param reply 自动回复 | |
| 43 | + * @param sendReqBO 消息内容 | |
| 39 | 44 | * @return 微信回复消息 XML |
| 40 | 45 | */ |
| 41 | - WxMpXmlOutMessage createFromAutoReply(String openid, MpAutoReplyDO reply); | |
| 46 | + WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO); | |
| 42 | 47 | |
| 43 | 48 | } | ... | ... |
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; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.io.FileTypeUtil; |
| 4 | 4 | import cn.hutool.core.io.FileUtil; |
| 5 | -import cn.hutool.core.io.IoUtil; | |
| 6 | 5 | import cn.hutool.core.lang.Assert; |
| 7 | 6 | import cn.hutool.core.util.StrUtil; |
| 8 | -import cn.iocoder.yudao.framework.common.util.io.FileUtils; | |
| 7 | +import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 9 | 8 | import cn.iocoder.yudao.module.infra.api.file.FileApi; |
| 9 | +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; | |
| 10 | 10 | import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert; |
| 11 | 11 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 12 | -import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO; | |
| 13 | 12 | import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; |
| 14 | 13 | import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; |
| 14 | +import cn.iocoder.yudao.module.mp.dal.mysql.message.MpMessageMapper; | |
| 15 | 15 | import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum; |
| 16 | 16 | import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; |
| 17 | +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; | |
| 17 | 18 | import cn.iocoder.yudao.module.mp.service.account.MpAccountService; |
| 19 | +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; | |
| 18 | 20 | import cn.iocoder.yudao.module.mp.service.user.MpUserService; |
| 19 | -import lombok.SneakyThrows; | |
| 20 | 21 | import lombok.extern.slf4j.Slf4j; |
| 21 | 22 | import me.chanjar.weixin.common.error.WxErrorException; |
| 22 | 23 | import me.chanjar.weixin.mp.api.WxMpService; |
| ... | ... | @@ -24,20 +25,12 @@ import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; |
| 24 | 25 | import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; |
| 25 | 26 | import org.springframework.context.annotation.Lazy; |
| 26 | 27 | import org.springframework.stereotype.Service; |
| 27 | - | |
| 28 | -import javax.annotation.Resource; | |
| 29 | - | |
| 30 | 28 | import org.springframework.validation.annotation.Validated; |
| 31 | 29 | |
| 32 | -import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*; | |
| 33 | -import cn.iocoder.yudao.framework.common.pojo.PageResult; | |
| 34 | - | |
| 35 | -import cn.iocoder.yudao.module.mp.dal.mysql.message.MpMessageMapper; | |
| 36 | - | |
| 30 | +import javax.annotation.Resource; | |
| 31 | +import javax.validation.Validator; | |
| 37 | 32 | import java.io.File; |
| 38 | 33 | |
| 39 | -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 40 | - | |
| 41 | 34 | /** |
| 42 | 35 | * 粉丝消息表 Service 实现类 |
| 43 | 36 | * |
| ... | ... | @@ -64,13 +57,16 @@ public class MpMessageServiceImpl implements MpMessageService { |
| 64 | 57 | @Resource |
| 65 | 58 | private FileApi fileApi; |
| 66 | 59 | |
| 60 | + @Resource | |
| 61 | + private Validator validator; | |
| 62 | + | |
| 67 | 63 | @Override |
| 68 | 64 | public PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO) { |
| 69 | 65 | return mpMessageMapper.selectPage(pageReqVO); |
| 70 | 66 | } |
| 71 | 67 | |
| 72 | 68 | @Override |
| 73 | - public void createFromUser(String appId, WxMpXmlMessage wxMessage) { | |
| 69 | + public void receiveMessage(String appId, WxMpXmlMessage wxMessage) { | |
| 74 | 70 | WxMpService mpService = mpServiceFactory.getRequiredMpService(appId); |
| 75 | 71 | // 获得关联信息 |
| 76 | 72 | MpAccountDO account = mpAccountService.getAccountFromCache(appId); |
| ... | ... | @@ -88,24 +84,21 @@ public class MpMessageServiceImpl implements MpMessageService { |
| 88 | 84 | message.setThumbMediaUrl(mediaDownload(mpService, message.getThumbMediaId())); |
| 89 | 85 | } |
| 90 | 86 | mpMessageMapper.insert(message); |
| 91 | - | |
| 92 | -// WxConsts.MenuButtonType.VIEW TODO 芋艿:待测试 | |
| 93 | -// wxMessage.getEventKey() | |
| 94 | - | |
| 95 | -// WxConsts.MenuButtonType.CLICK | |
| 96 | -// wxMessage.getEventKey() | |
| 97 | 87 | } |
| 98 | 88 | |
| 99 | 89 | @Override |
| 100 | - public WxMpXmlOutMessage createFromAutoReply(String openid, MpAutoReplyDO reply) { | |
| 90 | + public WxMpXmlOutMessage sendOutMessage(MpMessageSendOutReqBO sendReqBO) { | |
| 91 | + // 校验消息格式 | |
| 92 | + MpUtils.validateMessage(validator, sendReqBO.getType(), sendReqBO); | |
| 93 | + | |
| 101 | 94 | // 获得关联信息 |
| 102 | - MpAccountDO account = mpAccountService.getAccountFromCache(reply.getAppId()); | |
| 103 | - Assert.notNull(account, "公众号账号({}) 不存在", reply.getAppId()); | |
| 104 | - MpUserDO user = mpUserService.getUser(reply.getAppId(), openid); | |
| 105 | - Assert.notNull(user, "公众号粉丝({}/{}) 不存在", reply.getAppId(), openid); | |
| 95 | + MpAccountDO account = mpAccountService.getAccountFromCache(sendReqBO.getAppId()); | |
| 96 | + Assert.notNull(account, "公众号账号({}) 不存在", sendReqBO.getAppId()); | |
| 97 | + MpUserDO user = mpUserService.getUser(sendReqBO.getAppId(), sendReqBO.getOpenid()); | |
| 98 | + Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid()); | |
| 106 | 99 | |
| 107 | 100 | // 记录消息 |
| 108 | - MpMessageDO message = MpMessageConvert.INSTANCE.convert(reply, account, user); | |
| 101 | + MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user); | |
| 109 | 102 | message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); |
| 110 | 103 | mpMessageMapper.insert(message); |
| 111 | 104 | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.mp.service.message.bo; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; | |
| 4 | +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; | |
| 5 | +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*; | |
| 6 | +import lombok.Data; | |
| 7 | +import me.chanjar.weixin.common.api.WxConsts; | |
| 8 | + | |
| 9 | +import javax.validation.Valid; | |
| 10 | +import javax.validation.constraints.NotEmpty; | |
| 11 | +import javax.validation.constraints.NotNull; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * 公众号消息发送 Request BO | |
| 15 | + * | |
| 16 | + * 为什么要有该 BO 呢?在自动回复、客服消息、菜单回复消息等场景,都涉及到 MP 给用户发送消息,所以使用该 BO 统一承接 | |
| 17 | + * | |
| 18 | + * @author 芋道源码 | |
| 19 | + */ | |
| 20 | +@Data | |
| 21 | +public class MpMessageSendOutReqBO { | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * 公众号 appId | |
| 25 | + */ | |
| 26 | + @NotEmpty(message = "公众号 appId 不能为空") | |
| 27 | + private String appId; | |
| 28 | + /** | |
| 29 | + * 公众号用户 openid | |
| 30 | + */ | |
| 31 | + @NotEmpty(message = "公众号用户 openid 不能为空") | |
| 32 | + private String openid; | |
| 33 | + | |
| 34 | + // ========== 消息内容 ========== | |
| 35 | + /** | |
| 36 | + * 消息类型 | |
| 37 | + * | |
| 38 | + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS | |
| 39 | + */ | |
| 40 | + @NotEmpty(message = "消息类型不能为空") | |
| 41 | + public String type; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * 消息内容 | |
| 45 | + * | |
| 46 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT | |
| 47 | + */ | |
| 48 | + @NotEmpty(message = "消息内容不能为空", groups = TextGroup.class) | |
| 49 | + private String content; | |
| 50 | + | |
| 51 | + /** | |
| 52 | + * 媒体 id | |
| 53 | + * | |
| 54 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO | |
| 55 | + */ | |
| 56 | + @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) | |
| 57 | + private String mediaId; | |
| 58 | + /** | |
| 59 | + * 媒体 URL | |
| 60 | + * | |
| 61 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO | |
| 62 | + */ | |
| 63 | + @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) | |
| 64 | + private String mediaUrl; | |
| 65 | + | |
| 66 | + /** | |
| 67 | + * 标题 | |
| 68 | + * | |
| 69 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO | |
| 70 | + */ | |
| 71 | + @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) | |
| 72 | + private String title; | |
| 73 | + /** | |
| 74 | + * 描述 | |
| 75 | + * | |
| 76 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO | |
| 77 | + */ | |
| 78 | + @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) | |
| 79 | + private String description; | |
| 80 | + | |
| 81 | + /** | |
| 82 | + * 图文消息 | |
| 83 | + * | |
| 84 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS | |
| 85 | + */ | |
| 86 | + @Valid | |
| 87 | + @NotNull(message = "图文消息不能为空", groups = NewsGroup.class) | |
| 88 | + private MpMessageDO.Article article; | |
| 89 | + | |
| 90 | +} | ... | ... |