Commit 71beeabe9c57053f0e913fefccafdb454479f01d

Authored by YunaiV
1 parent 665f4b2b

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

yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http
... ... @@ -16,5 +16,19 @@ tenant-id: {{adminTenentId}}
16 16 "name":"搜索",
17 17 "type":"view",
18 18 "url":"http://www.soso.com/"
  19 + },
  20 + {
  21 + "name": "父按钮",
  22 + "subButtons": [
  23 + {
  24 + "type":"click",
  25 + "name":"归去来兮",
  26 + "key":"MUSIC"
  27 + },
  28 + {
  29 + "name":"不说",
  30 + "type":"view",
  31 + "url":"https://www.soso.com/"
  32 + }]
19 33 }]
20 34 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java
... ... @@ -18,7 +18,4 @@ public class MpMenuBaseVO {
18 18 @NotNull(message = "公众号账号的编号不能为空")
19 19 private Long accountId;
20 20  
21   - @NotNull(message = "按钮不能为空")
22   - private List<WxMenuButton> buttons;
23   -
24 21 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java
... ... @@ -2,6 +2,10 @@ package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
2 2  
3 3 import lombok.*;
4 4 import io.swagger.annotations.*;
  5 +import me.chanjar.weixin.common.bean.menu.WxMenuButton;
  6 +
  7 +import javax.validation.constraints.NotNull;
  8 +import java.util.List;
5 9  
6 10 @ApiModel("管理后台 - 微信菜单保存 Request VO")
7 11 @Data
... ... @@ -9,4 +13,7 @@ import io.swagger.annotations.*;
9 13 @ToString(callSuper = true)
10 14 public class MpMenuSaveReqVO extends MpMenuBaseVO {
11 15  
  16 + @NotNull(message = "按钮不能为空")
  17 + private List<WxMenuButton> buttons;
  18 +
12 19 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/menu/MpMenuConvert.java
... ... @@ -2,7 +2,10 @@ package cn.iocoder.yudao.module.mp.convert.menu;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4  
  5 +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
5 6 import org.mapstruct.Mapper;
  7 +import org.mapstruct.Mapping;
  8 +import org.mapstruct.Mappings;
6 9 import org.mapstruct.factory.Mappers;
7 10 import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.*;
8 11 import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
... ... @@ -16,4 +19,16 @@ public interface MpMenuConvert {
16 19  
17 20 MpMenuRespVO convert(MpMenuDO bean);
18 21  
  22 + @Mappings({
  23 + @Mapping(source = "menu.appId", target = "appId"),
  24 + @Mapping(source = "menu.replyMessageType", target = "type"),
  25 + @Mapping(source = "menu.replyContent", target = "content"),
  26 + @Mapping(source = "menu.replyMediaId", target = "mediaId"),
  27 + @Mapping(source = "menu.replyMediaUrl", target = "mediaUrl"),
  28 + @Mapping(source = "menu.replyTitle", target = "title"),
  29 + @Mapping(source = "menu.replyDescription", target = "description"),
  30 + @Mapping(source = "menu.replyArticles", target = "articles"),
  31 + })
  32 + MpMessageSendOutReqBO convert(String openid, MpMenuDO menu);
  33 +
19 34 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java
... ... @@ -20,7 +20,7 @@ public interface MpAutoReplyConvert {
20 20 @Mapping(source = "reply.responseMediaUrl", target = "mediaUrl"),
21 21 @Mapping(source = "reply.responseTitle", target = "title"),
22 22 @Mapping(source = "reply.responseDescription", target = "description"),
23   - @Mapping(source = "reply.responseArticle", target = "article"),
  23 + @Mapping(source = "reply.responseArticles", target = "articles"),
24 24 })
25 25 MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply);
26 26  
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java
... ... @@ -64,7 +64,7 @@ public interface MpMessageConvert {
64 64 .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription());
65 65 break;
66 66 case WxConsts.XmlMsgType.NEWS: // 5. 图文
67   - message.setArticles(Collections.singletonList(sendReqBO.getArticle()));
  67 + message.setArticles(sendReqBO.getArticles());
68 68 break;
69 69 default:
70 70 throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
... ...
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   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 4 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
6 5 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
... ... @@ -8,7 +7,6 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
8 7 import com.baomidou.mybatisplus.annotation.TableField;
9 8 import com.baomidou.mybatisplus.annotation.TableId;
10 9 import com.baomidou.mybatisplus.annotation.TableName;
11   -import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
12 10 import lombok.Data;
13 11 import lombok.EqualsAndHashCode;
14 12 import lombok.ToString;
... ... @@ -20,8 +18,6 @@ import java.util.List;
20 18 /**
21 19 * 微信菜单 DO
22 20 *
23   - * 一个公众号,只有一个 MpMenuDO 记录。一个公众号的多个菜单,对应到就是 {@link #buttons} 多个按钮
24   - *
25 21 * @author 芋道源码
26 22 */
27 23 @TableName(value = "mp_menu", autoResultMap = true)
... ... @@ -32,7 +28,12 @@ import java.util.List;
32 28 public class MpMenuDO extends BaseDO {
33 29  
34 30 /**
35   - * 主键
  31 + * 编号 - 顶级菜单
  32 + */
  33 + public static final Long ID_ROOT = 0L;
  34 +
  35 + /**
  36 + * 编号
36 37 */
37 38 @TableId
38 39 private Long id;
... ... @@ -50,127 +51,105 @@ public class MpMenuDO extends BaseDO {
50 51 private String appId;
51 52  
52 53 /**
53   - * 按钮列表
54   - */
55   - @TableField(typeHandler = ButtonTypeHandler.class)
56   - private List<Button> buttons;
57   - /**
58   - * 同步状态
59   - *
60   - * true - 已同步
61   - * false - 未同步
62   - */
63   - private Boolean syncStatus;
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   -
162   - // TODO @芋艿:可以找一些新的思路
163   - public static class ButtonTypeHandler extends AbstractJsonTypeHandler<List<Button>> {
164   -
165   - @Override
166   - protected List<Button> parse(String json) {
167   - return JsonUtils.parseArray(json, Button.class);
168   - }
169   -
170   - @Override
171   - protected String toJson(List<Button> obj) {
172   - return JsonUtils.toJsonString(obj);
173   - }
174   -
175   - }
  54 + * 菜单名称
  55 + */
  56 + private String name;
  57 + /**
  58 + * 菜单标识
  59 + *
  60 + * 支持多 DB 类型时,无法直接使用 key + @TableField("menuKey") 来实现转换,原因是 "menuKey" AS key 而存在报错
  61 + */
  62 + private String menuKey;
  63 + /**
  64 + * 父菜单编号
  65 + */
  66 + private Long parentId;
  67 + /**
  68 + * 排序
  69 + */
  70 + private Integer sort;
  71 +
  72 + // ========== 按钮操作 ==========
  73 +
  74 + /**
  75 + * 按钮类型
  76 + *
  77 + * 枚举 {@link MenuButtonType}
  78 + */
  79 + private String type;
  80 +
  81 + /**
  82 + * 网页链接
  83 + *
  84 + * 用户点击菜单可打开链接,不超过 1024 字节
  85 + *
  86 + * 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM
  87 + */
  88 + private String url;
  89 +
  90 + /**
  91 + * 小程序的 appId
  92 + *
  93 + * 类型为 {@link MenuButtonType} 的 MINIPROGRAM
  94 + */
  95 + private String miniProgramAppId;
  96 + /**
  97 + * 小程序的页面路径
  98 + *
  99 + * 类型为 {@link MenuButtonType} 的 MINIPROGRAM
  100 + */
  101 + private String miniProgramPagePath;
  102 +
  103 + // ========== 消息内容 ==========
  104 +
  105 + /**
  106 + * 消息类型
  107 + *
  108 + * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG
  109 + *
  110 + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS
  111 + */
  112 + private String replyMessageType;
  113 +
  114 + /**
  115 + * 回复的消息内容
  116 + *
  117 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT
  118 + */
  119 + private String replyContent;
  120 +
  121 + /**
  122 + * 回复的媒体 id
  123 + *
  124 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
  125 + */
  126 + private String replyMediaId;
  127 + /**
  128 + * 回复的媒体 URL
  129 + *
  130 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
  131 + */
  132 + private String replyMediaUrl;
  133 +
  134 + /**
  135 + * 回复的标题
  136 + *
  137 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
  138 + */
  139 + private String replyTitle;
  140 + /**
  141 + * 回复的描述
  142 + *
  143 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
  144 + */
  145 + private String replyDescription;
  146 +
  147 + /**
  148 + * 回复的图文消息数组
  149 + *
  150 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
  151 + */
  152 + @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class)
  153 + private List<MpMessageDO.Article> replyArticles;
  154 +
176 155 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java
... ... @@ -16,6 +16,7 @@ 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;
19 20 import java.util.Set;
20 21  
21 22 /**
... ... @@ -130,6 +131,6 @@ public class MpAutoReplyDO extends BaseDO {
130 131 *
131 132 * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
132 133 */
133   - @TableField(typeHandler = JacksonTypeHandler.class)
134   - private MpMessageDO.Article responseArticle;
  134 + @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class)
  135 + private List<MpMessageDO.Article> responseArticles;
135 136 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/menu/MpMenuMapper.java
... ... @@ -7,8 +7,9 @@ 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);
  10 + default MpMenuDO selectByAppIdAndMenuKey(String appId, String menuKey) {
  11 + return selectOne(MpMenuDO::getAppId, appId,
  12 + MpMenuDO::getMenuKey, menuKey);
12 13 }
13 14  
14 15 }
... ...
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 3 import cn.hutool.core.util.StrUtil;
6 4 import cn.iocoder.yudao.module.mp.convert.menu.MpMenuConvert;
7   -import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
8 5 import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
9 6 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
10   -import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
  7 +import cn.iocoder.yudao.module.mp.service.message.MpMessageService;
  8 +import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
11 9 import lombok.extern.slf4j.Slf4j;
12 10 import me.chanjar.weixin.common.bean.menu.WxMenu;
13 11 import me.chanjar.weixin.common.error.WxErrorException;
... ... @@ -38,6 +36,9 @@ import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*;
38 36 public class MpMenuServiceImpl implements MpMenuService {
39 37  
40 38 @Resource
  39 + private MpMessageService mpMessageService;
  40 +
  41 + @Resource
41 42 @Lazy // 延迟加载,避免循环引用报错
42 43 private MpServiceFactory mpServiceFactory;
43 44  
... ... @@ -87,47 +88,21 @@ public class MpMenuServiceImpl implements MpMenuService {
87 88  
88 89 @Override
89 90 public WxMpXmlOutMessage reply(String appId, String key, String openid) {
90   - // 获得菜单
91   - MpMenuDO menu = mpMenuMapper.selectByAppId(appId);
  91 + // 第一步,获得菜单
  92 + MpMenuDO menu = mpMenuMapper.selectByAppIdAndMenuKey(appId, key);
92 93 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);
  94 + log.error("[reply][appId({}) key({}) 找不到对应的菜单]", appId, key);
100 95 return null;
101 96 }
102 97 // 按钮必须要有消息类型,不然后续无法回复消息
103   - if (StrUtil.isEmpty(button.getMessageType())) {
104   - log.error("[reply][appId({}) key({}) 不存在消息类型({})]", appId, key, button);
  98 + if (StrUtil.isEmpty(menu.getReplyMessageType())) {
  99 + log.error("[reply][menu({}) 不存在对应的消息类型]", menu);
105 100 return null;
106 101 }
107 102  
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;
  103 + // 第二步,回复消息
  104 + MpMessageSendOutReqBO sendReqBO = MpMenuConvert.INSTANCE.convert(openid, menu);
  105 + return mpMessageService.sendOutMessage(sendReqBO);
131 106 }
132 107  
133 108 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java
... ... @@ -9,6 +9,7 @@ import me.chanjar.weixin.common.api.WxConsts;
9 9 import javax.validation.Valid;
10 10 import javax.validation.constraints.NotEmpty;
11 11 import javax.validation.constraints.NotNull;
  12 +import java.util.List;
12 13  
13 14 /**
14 15 * 公众号消息发送 Request BO
... ... @@ -85,6 +86,6 @@ public class MpMessageSendOutReqBO {
85 86 */
86 87 @Valid
87 88 @NotNull(message = "图文消息不能为空", groups = NewsGroup.class)
88   - private MpMessageDO.Article article;
  89 + private List<MpMessageDO.Article> articles;
89 90  
90 91 }
... ...