Commit 665f4b2b0926f4cf10b295724ffa06be04064a92

Authored by YunaiV
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
... ... @@ -16,7 +16,6 @@ import lombok.ToString;
16 16 import me.chanjar.weixin.common.api.WxConsts;
17 17 import me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
18 18  
19   -import java.util.List;
20 19 import java.util.Set;
21 20  
22 21 /**
... ...
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
... ... @@ -7,4 +7,8 @@ import org.apache.ibatis.annotations.Mapper;
7 7 @Mapper
8 8 public interface MpMenuMapper extends BaseMapperX<MpMenuDO> {
9 9  
  10 + default MpMenuDO selectByAppId(String appId) {
  11 + return selectOne(MpMenuDO::getAppId, appId);
  12 + }
  13 +
10 14 }
... ...
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 +}
... ...