Commit 0499226c3d3f15fb8b0581b01e46d207c7a4be3f

Authored by YunaiV
1 parent 141e4e4c

mp:完善【菜单】的回复功能

yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java
1 package cn.iocoder.yudao.module.mp.controller.admin.menu.vo; 1 package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
2 2
3 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; 3 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
  4 +import io.swagger.annotations.ApiModelProperty;
4 import lombok.Data; 5 import lombok.Data;
5 import me.chanjar.weixin.common.api.WxConsts; 6 import me.chanjar.weixin.common.api.WxConsts;
  7 +import org.hibernate.validator.constraints.URL;
6 8
  9 +import javax.validation.Valid;
  10 +import javax.validation.constraints.NotEmpty;
  11 +import javax.validation.constraints.NotNull;
7 import java.util.List; 12 import java.util.List;
8 13
9 -// TODO 芋艿:完善 swagger 注解 14 +import static cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
  15 +
10 /** 16 /**
11 * 微信菜单 Base VO,提供给添加、修改、详细的子 VO 使用 17 * 微信菜单 Base VO,提供给添加、修改、详细的子 VO 使用
12 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 18 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
@@ -38,70 +44,51 @@ public class MpMenuBaseVO { @@ -38,70 +44,51 @@ public class MpMenuBaseVO {
38 */ 44 */
39 private String type; 45 private String type;
40 46
41 - /**  
42 - * 网页链接  
43 - *  
44 - * 用户点击菜单可打开链接,不超过 1024 字节  
45 - *  
46 - * 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM  
47 - */ 47 + @ApiModelProperty(value = "网页链接", example = "https://www.iocoder.cn/")
  48 + @NotEmpty(message = "网页链接不能为空", groups = {ViewButtonGroup.class, MiniProgramButtonGroup.class})
  49 + @URL(message = "网页链接必须是 URL 格式")
48 private String url; 50 private String url;
49 51
50 - /**  
51 - * 小程序的 appId  
52 - *  
53 - * 类型为 {@link WxConsts.MenuButtonType} 的 MINIPROGRAM  
54 - */ 52 + @ApiModelProperty(value = "小程序的 appId", example = "wx1234567890")
  53 + @NotEmpty(message = "小程序的 appId 不能为空", groups = MiniProgramButtonGroup.class)
55 private String miniProgramAppId; 54 private String miniProgramAppId;
56 - /**  
57 - * 小程序的页面路径  
58 - *  
59 - * 类型为 {@link WxConsts.MenuButtonType} 的 MINIPROGRAM  
60 - */ 55 +
  56 + @ApiModelProperty(value = "小程序的页面路径", example = "pages/index/index")
  57 + @NotEmpty(message = "小程序的页面路径不能为空", groups = MiniProgramButtonGroup.class)
61 private String miniProgramPagePath; 58 private String miniProgramPagePath;
62 59
63 // ========== 消息内容 ========== 60 // ========== 消息内容 ==========
64 61
65 - /**  
66 - * 消息类型  
67 - *  
68 - * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG  
69 - *  
70 - * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS  
71 - */ 62 + @ApiModelProperty(value = "消息类型", example = "text",
  63 + notes = "枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC")
  64 + @NotEmpty(message = "消息类型不能为空", groups = {ClickButtonGroup.class, ScanCodeWaitMsgButtonGroup.class})
72 private String replyMessageType; 65 private String replyMessageType;
73 66
74 - /**  
75 - * 回复的消息内容  
76 - *  
77 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT  
78 - */ 67 + @ApiModelProperty(value = "回复的消息内容", example = "欢迎关注")
  68 + @NotEmpty(message = "回复的消息内容不能为空", groups = {TextMessageGroup.class})
79 private String replyContent; 69 private String replyContent;
80 70
81 - /**  
82 - * 回复的媒体 id  
83 - *  
84 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO  
85 - */ 71 + @ApiModelProperty(value = "回复的媒体 id", example = "123456")
  72 + @NotEmpty(message = "回复的消息 mediaId 不能为空",
  73 + groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
86 private String replyMediaId; 74 private String replyMediaId;
87 - /**  
88 - * 回复的媒体 URL  
89 - *  
90 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO  
91 - */ 75 + @ApiModelProperty(value = "回复的媒体 URL", example = "https://www.iocoder.cn/xxx.jpg")
  76 + @NotEmpty(message = "回复的消息 mediaId 不能为空",
  77 + groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
92 private String replyMediaUrl; 78 private String replyMediaUrl;
93 79
94 - /**  
95 - * 回复的标题  
96 - *  
97 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO  
98 - */ 80 + @ApiModelProperty(value = "缩略图的媒体 id", example = "123456")
  81 + @NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class})
  82 + private String replyThumbMediaId;
  83 + @ApiModelProperty(value = "缩略图的媒体 URL",example = "https://www.iocoder.cn/xxx.jpg")
  84 + @NotEmpty(message = "回复的消息 thumbMedia 地址不能为空", groups = {MusicMessageGroup.class})
  85 + private String replyThumbMediaUrl;
  86 +
  87 + @ApiModelProperty(value = "回复的标题", example = "视频标题")
  88 + @NotEmpty(message = "回复的消息标题不能为空", groups = VideoMessageGroup.class)
99 private String replyTitle; 89 private String replyTitle;
100 - /**  
101 - * 回复的描述  
102 - *  
103 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO  
104 - */ 90 + @ApiModelProperty(value = "回复的描述", example = "视频描述")
  91 + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
105 private String replyDescription; 92 private String replyDescription;
106 93
107 /** 94 /**
@@ -109,6 +96,17 @@ public class MpMenuBaseVO { @@ -109,6 +96,17 @@ public class MpMenuBaseVO {
109 * 96 *
110 * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS 97 * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
111 */ 98 */
  99 + @NotNull(message = "回复的图文消息不能为空", groups = NewsMessageGroup.class)
  100 + @Valid
112 private List<MpMessageDO.Article> replyArticles; 101 private List<MpMessageDO.Article> replyArticles;
113 102
  103 + @ApiModelProperty(value = "音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
  104 + @NotEmpty(message = "回复的音乐链接不能为空", groups = MusicMessageGroup.class)
  105 + @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
  106 + private String replyMusicUrl;
  107 + @ApiModelProperty(value = "高质量音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
  108 + @NotEmpty(message = "回复的高质量音乐链接不能为空", groups = MusicMessageGroup.class)
  109 + @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
  110 + private String replyHqMusicUrl;
  111 +
114 } 112 }
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/MpMessageSendReqVO.java
@@ -26,28 +26,28 @@ public class MpMessageSendReqVO { @@ -26,28 +26,28 @@ public class MpMessageSendReqVO {
26 public String type; 26 public String type;
27 27
28 @ApiModelProperty(value = "消息内容", required = true, example = "你好呀") 28 @ApiModelProperty(value = "消息内容", required = true, example = "你好呀")
29 - @NotEmpty(message = "消息内容不能为空", groups = TextGroup.class) 29 + @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class)
30 private String content; 30 private String content;
31 31
32 @ApiModelProperty(value = "媒体 ID", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP") 32 @ApiModelProperty(value = "媒体 ID", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
33 - @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) 33 + @NotEmpty(message = "消息内容不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
34 private String mediaId; 34 private String mediaId;
35 35
36 @ApiModelProperty(value = "标题", required = true, example = "没有标题") 36 @ApiModelProperty(value = "标题", required = true, example = "没有标题")
37 - @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) 37 + @NotEmpty(message = "消息内容不能为空", groups = VideoMessageGroup.class)
38 private String title; 38 private String title;
39 39
40 @ApiModelProperty(value = "描述", required = true, example = "你猜") 40 @ApiModelProperty(value = "描述", required = true, example = "你猜")
41 - @NotEmpty(message = "消息描述不能为空", groups = VideoGroup.class) 41 + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
42 private String description; 42 private String description;
43 43
44 @ApiModelProperty(value = "缩略图的媒体 id", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP") 44 @ApiModelProperty(value = "缩略图的媒体 id", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
45 - @NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicGroup.class) 45 + @NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicMessageGroup.class)
46 private String thumbMediaId; 46 private String thumbMediaId;
47 47
48 @ApiModelProperty(value = "图文消息", required = true) 48 @ApiModelProperty(value = "图文消息", required = true)
49 @Valid 49 @Valid
50 - @NotNull(message = "图文消息不能为空", groups = NewsGroup.class) 50 + @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class)
51 private List<MpMessageDO.Article> articles; 51 private List<MpMessageDO.Article> articles;
52 52
53 @ApiModelProperty(value = "音乐链接", example = "https://www.iocoder.cn/music.mp3", notes = "消息类型为 MUSIC 时") 53 @ApiModelProperty(value = "音乐链接", example = "https://www.iocoder.cn/music.mp3", notes = "消息类型为 MUSIC 时")
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/menu/MpMenuConvert.java
@@ -26,9 +26,12 @@ public interface MpMenuConvert { @@ -26,9 +26,12 @@ public interface MpMenuConvert {
26 @Mapping(source = "menu.replyMessageType", target = "type"), 26 @Mapping(source = "menu.replyMessageType", target = "type"),
27 @Mapping(source = "menu.replyContent", target = "content"), 27 @Mapping(source = "menu.replyContent", target = "content"),
28 @Mapping(source = "menu.replyMediaId", target = "mediaId"), 28 @Mapping(source = "menu.replyMediaId", target = "mediaId"),
  29 + @Mapping(source = "menu.replyThumbMediaId", target = "thumbMediaId"),
29 @Mapping(source = "menu.replyTitle", target = "title"), 30 @Mapping(source = "menu.replyTitle", target = "title"),
30 @Mapping(source = "menu.replyDescription", target = "description"), 31 @Mapping(source = "menu.replyDescription", target = "description"),
31 @Mapping(source = "menu.replyArticles", target = "articles"), 32 @Mapping(source = "menu.replyArticles", target = "articles"),
  33 + @Mapping(source = "menu.replyMusicUrl", target = "musicUrl"),
  34 + @Mapping(source = "menu.replyHqMusicUrl", target = "hqMusicUrl"),
32 }) 35 })
33 MpMessageSendOutReqBO convert(String openid, MpMenuDO menu); 36 MpMessageSendOutReqBO convert(String openid, MpMenuDO menu);
34 37
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpAutoReplyConvert.java
@@ -17,7 +17,6 @@ public interface MpAutoReplyConvert { @@ -17,7 +17,6 @@ public interface MpAutoReplyConvert {
17 @Mapping(source = "reply.responseMessageType", target = "type"), 17 @Mapping(source = "reply.responseMessageType", target = "type"),
18 @Mapping(source = "reply.responseContent", target = "content"), 18 @Mapping(source = "reply.responseContent", target = "content"),
19 @Mapping(source = "reply.responseMediaId", target = "mediaId"), 19 @Mapping(source = "reply.responseMediaId", target = "mediaId"),
20 - @Mapping(source = "reply.responseMediaUrl", target = "mediaUrl"),  
21 @Mapping(source = "reply.responseTitle", target = "title"), 20 @Mapping(source = "reply.responseTitle", target = "title"),
22 @Mapping(source = "reply.responseDescription", target = "description"), 21 @Mapping(source = "reply.responseDescription", target = "description"),
23 @Mapping(source = "reply.responseArticles", target = "articles"), 22 @Mapping(source = "reply.responseArticles", target = "articles"),
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java
@@ -57,10 +57,14 @@ public interface MpMessageConvert { @@ -57,10 +57,14 @@ public interface MpMessageConvert {
57 break; 57 break;
58 case WxConsts.XmlMsgType.IMAGE: // 2. 图片 58 case WxConsts.XmlMsgType.IMAGE: // 2. 图片
59 case WxConsts.XmlMsgType.VOICE: // 3. 语音 59 case WxConsts.XmlMsgType.VOICE: // 3. 语音
60 - message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl()); 60 + message.setMediaId(sendReqBO.getMediaId())
  61 +// .setMediaUrl(sendReqBO.getMediaUrl()) TODO 芋艿:去 url
  62 + ;
61 break; 63 break;
62 case WxConsts.XmlMsgType.VIDEO: // 4. 视频 64 case WxConsts.XmlMsgType.VIDEO: // 4. 视频
63 - message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl()) 65 + message.setMediaId(sendReqBO.getMediaId())
  66 +// .setMediaUrl(sendReqBO.getMediaUrl()) TODO 芋艿:去 url
  67 +
64 .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()); 68 .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription());
65 break; 69 break;
66 case WxConsts.XmlMsgType.NEWS: // 5. 图文 70 case WxConsts.XmlMsgType.NEWS: // 5. 图文
@@ -69,7 +73,7 @@ public interface MpMessageConvert { @@ -69,7 +73,7 @@ public interface MpMessageConvert {
69 message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()) 73 message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription())
70 .setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl()) 74 .setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl())
71 .setThumbMediaId(sendReqBO.getThumbMediaId()); 75 .setThumbMediaId(sendReqBO.getThumbMediaId());
72 -// .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:url 待确定 76 +// .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:去 url
73 break; 77 break;
74 default: 78 default:
75 throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); 79 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
@@ -103,7 +103,7 @@ public class MpMenuDO extends BaseDO { @@ -103,7 +103,7 @@ public class MpMenuDO extends BaseDO {
103 * 103 *
104 * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG 104 * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG
105 * 105 *
106 - * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS 106 + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC
107 */ 107 */
108 private String replyMessageType; 108 private String replyMessageType;
109 109
@@ -141,6 +141,19 @@ public class MpMenuDO extends BaseDO { @@ -141,6 +141,19 @@ public class MpMenuDO extends BaseDO {
141 private String replyDescription; 141 private String replyDescription;
142 142
143 /** 143 /**
  144 + * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
  145 + *
  146 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
  147 + */
  148 + private String replyThumbMediaId;
  149 + /**
  150 + * 缩略图的媒体 URL
  151 + *
  152 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
  153 + */
  154 + private String replyThumbMediaUrl;
  155 +
  156 + /**
144 * 回复的图文消息数组 157 * 回复的图文消息数组
145 * 158 *
146 * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS 159 * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
@@ -148,4 +161,19 @@ public class MpMenuDO extends BaseDO { @@ -148,4 +161,19 @@ public class MpMenuDO extends BaseDO {
148 @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) 161 @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class)
149 private List<MpMessageDO.Article> replyArticles; 162 private List<MpMessageDO.Article> replyArticles;
150 163
  164 + /**
  165 + * 回复的音乐链接
  166 + *
  167 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
  168 + */
  169 + private String replyMusicUrl;
  170 + /**
  171 + * 回复的高质量音乐链接
  172 + *
  173 + * WIFI 环境优先使用该链接播放音乐
  174 + *
  175 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
  176 + */
  177 + private String replyHqMusicUrl;
  178 +
151 } 179 }
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java
@@ -124,13 +124,13 @@ public class MpMessageDO extends BaseDO { @@ -124,13 +124,13 @@ public class MpMessageDO extends BaseDO {
124 /** 124 /**
125 * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id 125 * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
126 * 126 *
127 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC 127 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
128 */ 128 */
129 private String thumbMediaId; 129 private String thumbMediaId;
130 /** 130 /**
131 * 缩略图的媒体 URL 131 * 缩略图的媒体 URL
132 * 132 *
133 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO 133 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
134 */ 134 */
135 private String thumbMediaUrl; 135 private String thumbMediaUrl;
136 136
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java
1 package cn.iocoder.yudao.module.mp.framework.mp.core.util; 1 package cn.iocoder.yudao.module.mp.framework.mp.core.util;
2 2
  3 +import cn.hutool.core.util.StrUtil;
3 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; 4 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
4 import lombok.extern.slf4j.Slf4j; 5 import lombok.extern.slf4j.Slf4j;
5 import me.chanjar.weixin.common.api.WxConsts; 6 import me.chanjar.weixin.common.api.WxConsts;
@@ -15,36 +16,6 @@ import javax.validation.Validator; @@ -15,36 +16,6 @@ import javax.validation.Validator;
15 public class MpUtils { 16 public class MpUtils {
16 17
17 /** 18 /**
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 - * Music 类型的消息,参数校验 Group  
44 - */  
45 - public interface MusicGroup {}  
46 -  
47 - /**  
48 * 校验消息的格式是否符合要求 19 * 校验消息的格式是否符合要求
49 * 20 *
50 * @param type 类型 21 * @param type 类型
@@ -55,22 +26,22 @@ public class MpUtils { @@ -55,22 +26,22 @@ public class MpUtils {
55 Class<?> group; 26 Class<?> group;
56 switch (type) { 27 switch (type) {
57 case WxConsts.XmlMsgType.TEXT: 28 case WxConsts.XmlMsgType.TEXT:
58 - group = TextGroup.class; 29 + group = TextMessageGroup.class;
59 break; 30 break;
60 case WxConsts.XmlMsgType.IMAGE: 31 case WxConsts.XmlMsgType.IMAGE:
61 - group = ImageGroup.class; 32 + group = ImageMessageGroup.class;
62 break; 33 break;
63 case WxConsts.XmlMsgType.VOICE: 34 case WxConsts.XmlMsgType.VOICE:
64 - group = VoiceGroup.class; 35 + group = VoiceMessageGroup.class;
65 break; 36 break;
66 case WxConsts.XmlMsgType.VIDEO: 37 case WxConsts.XmlMsgType.VIDEO:
67 - group = VideoGroup.class; 38 + group = VideoMessageGroup.class;
68 break; 39 break;
69 case WxConsts.XmlMsgType.NEWS: 40 case WxConsts.XmlMsgType.NEWS:
70 - group = NewsGroup.class; 41 + group = NewsMessageGroup.class;
71 break; 42 break;
72 case WxConsts.XmlMsgType.MUSIC: 43 case WxConsts.XmlMsgType.MUSIC:
73 - group = MusicGroup.class; 44 + group = MusicMessageGroup.class;
74 break; 45 break;
75 default: 46 default:
76 log.error("[validateMessage][未知的消息类型({})]", message); 47 log.error("[validateMessage][未知的消息类型({})]", message);
@@ -80,6 +51,35 @@ public class MpUtils { @@ -80,6 +51,35 @@ public class MpUtils {
80 ValidationUtils.validate(validator, message, group); 51 ValidationUtils.validate(validator, message, group);
81 } 52 }
82 53
  54 + public static void validateButton(Validator validator, String type, String messageType, Object button) {
  55 + if (StrUtil.isBlank(type)) {
  56 + return;
  57 + }
  58 + // 获得对应的校验 group
  59 + Class<?> group;
  60 + switch (type) {
  61 + case WxConsts.MenuButtonType.CLICK:
  62 + group = ClickButtonGroup.class;
  63 + validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式
  64 + break;
  65 + case WxConsts.MenuButtonType.VIEW:
  66 + group = ViewButtonGroup.class;
  67 + break;
  68 + case WxConsts.MenuButtonType.MINIPROGRAM:
  69 + group = MiniProgramButtonGroup.class;
  70 + break;
  71 + case WxConsts.MenuButtonType.SCANCODE_WAITMSG:
  72 + group = ScanCodeWaitMsgButtonGroup.class;
  73 + validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式
  74 + break;
  75 + default:
  76 + log.error("[validateButton][未知的按钮({})]", button);
  77 + throw new IllegalArgumentException("不支持的按钮类型:" + type);
  78 + }
  79 + // 执行校验
  80 + ValidationUtils.validate(validator, button, group);
  81 + }
  82 +
83 /** 83 /**
84 * 根据消息类型,获得对应的媒体文件类型 84 * 根据消息类型,获得对应的媒体文件类型
85 * 85 *
@@ -101,4 +101,53 @@ public class MpUtils { @@ -101,4 +101,53 @@ public class MpUtils {
101 } 101 }
102 } 102 }
103 103
  104 + /**
  105 + * Text 类型的消息,参数校验 Group
  106 + */
  107 + public interface TextMessageGroup {}
  108 +
  109 + /**
  110 + * Image 类型的消息,参数校验 Group
  111 + */
  112 + public interface ImageMessageGroup {}
  113 +
  114 + /**
  115 + * Voice 类型的消息,参数校验 Group
  116 + */
  117 + public interface VoiceMessageGroup {}
  118 +
  119 + /**
  120 + * Video 类型的消息,参数校验 Group
  121 + */
  122 + public interface VideoMessageGroup {}
  123 +
  124 + /**
  125 + * News 类型的消息,参数校验 Group
  126 + */
  127 + public interface NewsMessageGroup {}
  128 +
  129 + /**
  130 + * Music 类型的消息,参数校验 Group
  131 + */
  132 + public interface MusicMessageGroup {}
  133 +
  134 + /**
  135 + * Click 类型的按钮,参数校验 Group
  136 + */
  137 + public interface ClickButtonGroup {}
  138 +
  139 + /**
  140 + * View 类型的按钮,参数校验 Group
  141 + */
  142 + public interface ViewButtonGroup {}
  143 +
  144 + /**
  145 + * MiniProgram 类型的按钮,参数校验 Group
  146 + */
  147 + public interface MiniProgramButtonGroup {}
  148 +
  149 + /**
  150 + * SCANCODE_WAITMSG 类型的按钮,参数校验 Group
  151 + */
  152 + public interface ScanCodeWaitMsgButtonGroup {}
104 } 153 }
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/menu/MpMenuServiceImpl.java
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
8 import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; 8 import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
9 import cn.iocoder.yudao.module.mp.dal.mysql.menu.MpMenuMapper; 9 import cn.iocoder.yudao.module.mp.dal.mysql.menu.MpMenuMapper;
10 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; 10 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
  11 +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils;
11 import cn.iocoder.yudao.module.mp.service.account.MpAccountService; 12 import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
12 import cn.iocoder.yudao.module.mp.service.message.MpMessageService; 13 import cn.iocoder.yudao.module.mp.service.message.MpMessageService;
13 import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; 14 import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
@@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional; @@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
22 import org.springframework.validation.annotation.Validated; 23 import org.springframework.validation.annotation.Validated;
23 24
24 import javax.annotation.Resource; 25 import javax.annotation.Resource;
  26 +import javax.validation.Validator;
25 import java.util.List; 27 import java.util.List;
26 28
27 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -49,6 +51,9 @@ public class MpMenuServiceImpl implements MpMenuService { @@ -49,6 +51,9 @@ public class MpMenuServiceImpl implements MpMenuService {
49 private MpServiceFactory mpServiceFactory; 51 private MpServiceFactory mpServiceFactory;
50 52
51 @Resource 53 @Resource
  54 + private Validator validator;
  55 +
  56 + @Resource
52 private MpMenuMapper mpMenuMapper; 57 private MpMenuMapper mpMenuMapper;
53 58
54 @Override 59 @Override
@@ -57,6 +62,9 @@ public class MpMenuServiceImpl implements MpMenuService { @@ -57,6 +62,9 @@ public class MpMenuServiceImpl implements MpMenuService {
57 MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); 62 MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId());
58 WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId()); 63 WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId());
59 64
  65 + // 参数校验
  66 + createReqVO.getMenus().forEach(this::validateMenu);
  67 +
60 // 第一步,同步公众号 68 // 第一步,同步公众号
61 WxMenu wxMenu = new WxMenu(); 69 WxMenu wxMenu = new WxMenu();
62 wxMenu.setButtons(MpMenuConvert.INSTANCE.convert(createReqVO.getMenus())); 70 wxMenu.setButtons(MpMenuConvert.INSTANCE.convert(createReqVO.getMenus()));
@@ -79,40 +87,64 @@ public class MpMenuServiceImpl implements MpMenuService { @@ -79,40 +87,64 @@ public class MpMenuServiceImpl implements MpMenuService {
79 }); 87 });
80 } 88 }
81 89
82 - @Override  
83 - public void deleteMenuByAccountId(Long accountId) {  
84 - WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);  
85 - // 第一步,同步公众号  
86 - try {  
87 - mpService.getMenuService().menuDelete();  
88 - } catch (WxErrorException e) {  
89 - throw exception(MENU_DELETE_FAIL, e.getError().getErrorMsg()); 90 + /**
  91 + * 校验菜单的格式是否正确
  92 + *
  93 + * @param menu 菜单
  94 + */
  95 + private void validateMenu(MpMenuSaveReqVO.Menu menu) {
  96 + MpUtils.validateButton(validator, menu.getType(), menu.getReplyMessageType(), menu);
  97 + // 子菜单
  98 + if (CollUtil.isEmpty(menu.getChildren())) {
  99 + return;
90 } 100 }
91 -  
92 - // 第二步,存储到数据库  
93 - mpMenuMapper.deleteByAccountId(accountId); 101 + menu.getChildren().forEach(this::validateMenu);
94 } 102 }
95 103
  104 + /**
  105 + * 创建菜单,并存储到数据库
  106 + *
  107 + * @param wxMenu 菜单信息
  108 + * @param parentMenu 父菜单
  109 + * @param account 公众号账号
  110 + * @return 创建后的菜单
  111 + */
96 private MpMenuDO createMenu(MpMenuSaveReqVO.Menu wxMenu, MpMenuDO parentMenu, MpAccountDO account) { 112 private MpMenuDO createMenu(MpMenuSaveReqVO.Menu wxMenu, MpMenuDO parentMenu, MpAccountDO account) {
  113 + // 创建菜单
97 MpMenuDO menu = CollUtil.isNotEmpty(wxMenu.getChildren()) 114 MpMenuDO menu = CollUtil.isNotEmpty(wxMenu.getChildren())
98 ? new MpMenuDO().setName(wxMenu.getName()) 115 ? new MpMenuDO().setName(wxMenu.getName())
99 : MpMenuConvert.INSTANCE.convert02(wxMenu); 116 : MpMenuConvert.INSTANCE.convert02(wxMenu);
  117 + // 设置菜单的公众号账号信息
100 if (account != null) { 118 if (account != null) {
101 menu.setAccountId(account.getId()).setAppId(account.getAppId()); 119 menu.setAccountId(account.getId()).setAppId(account.getAppId());
102 } 120 }
  121 + // 设置父编号
103 if (parentMenu != null) { 122 if (parentMenu != null) {
104 menu.setParentId(parentMenu.getId()); 123 menu.setParentId(parentMenu.getId());
105 } else { 124 } else {
106 menu.setParentId(MpMenuDO.ID_ROOT); 125 menu.setParentId(MpMenuDO.ID_ROOT);
107 } 126 }
108 - if (StrUtil.isNotEmpty(wxMenu.getReplyMediaId())) {  
109 - throw new IllegalArgumentException("未实现");  
110 - } 127 +
  128 + // 插入到数据库
111 mpMenuMapper.insert(menu); 129 mpMenuMapper.insert(menu);
112 return menu; 130 return menu;
113 } 131 }
114 132
115 @Override 133 @Override
  134 + public void deleteMenuByAccountId(Long accountId) {
  135 + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
  136 + // 第一步,同步公众号
  137 + try {
  138 + mpService.getMenuService().menuDelete();
  139 + } catch (WxErrorException e) {
  140 + throw exception(MENU_DELETE_FAIL, e.getError().getErrorMsg());
  141 + }
  142 +
  143 + // 第二步,存储到数据库
  144 + mpMenuMapper.deleteByAccountId(accountId);
  145 + }
  146 +
  147 + @Override
116 public WxMpXmlOutMessage reply(String appId, String key, String openid) { 148 public WxMpXmlOutMessage reply(String appId, String key, String openid) {
117 // 第一步,获得菜单 149 // 第一步,获得菜单
118 MpMenuDO menu = mpMenuMapper.selectByAppIdAndMenuKey(appId, key); 150 MpMenuDO menu = mpMenuMapper.selectByAppIdAndMenuKey(appId, key);
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java
@@ -76,8 +76,8 @@ public class MpMessageServiceImpl implements MpMessageService { @@ -76,8 +76,8 @@ public class MpMessageServiceImpl implements MpMessageService {
76 Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser()); 76 Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser());
77 77
78 // 记录消息 78 // 记录消息
79 - MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);  
80 - message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom()); 79 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user)
  80 + .setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
81 downloadMessageMedia(message); 81 downloadMessageMedia(message);
82 mpMessageMapper.insert(message); 82 mpMessageMapper.insert(message);
83 } 83 }
@@ -94,9 +94,9 @@ public class MpMessageServiceImpl implements MpMessageService { @@ -94,9 +94,9 @@ public class MpMessageServiceImpl implements MpMessageService {
94 Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid()); 94 Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid());
95 95
96 // 记录消息 96 // 记录消息
97 - MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user);  
98 - message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());  
99 - // TODO 芋艿:downloadMessageMedia 97 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user).
  98 + setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
  99 + downloadMessageMedia(message);
100 mpMessageMapper.insert(message); 100 mpMessageMapper.insert(message);
101 101
102 // 转换返回 WxMpXmlOutMessage 对象 102 // 转换返回 WxMpXmlOutMessage 对象
@@ -122,8 +122,8 @@ public class MpMessageServiceImpl implements MpMessageService { @@ -122,8 +122,8 @@ public class MpMessageServiceImpl implements MpMessageService {
122 } 122 }
123 123
124 // 记录消息 124 // 记录消息
125 - MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);  
126 - message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); 125 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user)
  126 + .setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
127 downloadMessageMedia(message); 127 downloadMessageMedia(message);
128 mpMessageMapper.insert(message); 128 mpMessageMapper.insert(message);
129 return message; 129 return message;
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java
@@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; @@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
4 import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*; 4 import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
5 import lombok.Data; 5 import lombok.Data;
6 import me.chanjar.weixin.common.api.WxConsts; 6 import me.chanjar.weixin.common.api.WxConsts;
  7 +import org.hibernate.validator.constraints.URL;
7 8
8 import javax.validation.Valid; 9 import javax.validation.Valid;
9 import javax.validation.constraints.NotEmpty; 10 import javax.validation.constraints.NotEmpty;
@@ -45,7 +46,7 @@ public class MpMessageSendOutReqBO { @@ -45,7 +46,7 @@ public class MpMessageSendOutReqBO {
45 * 46 *
46 * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT 47 * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT
47 */ 48 */
48 - @NotEmpty(message = "消息内容不能为空", groups = TextGroup.class) 49 + @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class)
49 private String content; 50 private String content;
50 51
51 /** 52 /**
@@ -53,39 +54,38 @@ public class MpMessageSendOutReqBO { @@ -53,39 +54,38 @@ public class MpMessageSendOutReqBO {
53 * 54 *
54 * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO 55 * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
55 */ 56 */
56 - @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) 57 + @NotEmpty(message = "消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
57 private String mediaId; 58 private String mediaId;
58 - // TODO 芋艿:考虑去掉  
59 - /**  
60 - * 媒体 URL  
61 - *  
62 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO  
63 - */  
64 - @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})  
65 - private String mediaUrl; 59 +// // TODO 芋艿:考虑去掉
  60 +// /**
  61 +// * 媒体 URL
  62 +// *
  63 +// * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
  64 +// */
  65 +// @NotEmpty(message = "消息内容不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
  66 +// private String mediaUrl;
66 67
67 /** 68 /**
68 * 缩略图的媒体 id 69 * 缩略图的媒体 id
69 * 70 *
70 * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC 71 * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC
71 */ 72 */
72 - @NotEmpty(message = "消息内容不能为空", groups = {MusicGroup.class}) 73 + @NotEmpty(message = "消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class})
73 private String thumbMediaId; 74 private String thumbMediaId;
74 - // TODO 芋艿:考虑去掉  
75 75
76 /** 76 /**
77 * 标题 77 * 标题
78 * 78 *
79 * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO 79 * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
80 */ 80 */
81 - @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) 81 + @NotEmpty(message = "消息标题不能为空", groups = VideoMessageGroup.class)
82 private String title; 82 private String title;
83 /** 83 /**
84 * 描述 84 * 描述
85 * 85 *
86 * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO 86 * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
87 */ 87 */
88 - @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) 88 + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
89 private String description; 89 private String description;
90 90
91 /** 91 /**
@@ -94,7 +94,7 @@ public class MpMessageSendOutReqBO { @@ -94,7 +94,7 @@ public class MpMessageSendOutReqBO {
94 * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS 94 * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
95 */ 95 */
96 @Valid 96 @Valid
97 - @NotNull(message = "图文消息不能为空", groups = NewsGroup.class) 97 + @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class)
98 private List<MpMessageDO.Article> articles; 98 private List<MpMessageDO.Article> articles;
99 99
100 /** 100 /**
@@ -102,6 +102,8 @@ public class MpMessageSendOutReqBO { @@ -102,6 +102,8 @@ public class MpMessageSendOutReqBO {
102 * 102 *
103 * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC 103 * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
104 */ 104 */
  105 + @NotEmpty(message = "音乐链接不能为空", groups = MusicMessageGroup.class)
  106 + @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
105 private String musicUrl; 107 private String musicUrl;
106 108
107 /** 109 /**
@@ -109,6 +111,8 @@ public class MpMessageSendOutReqBO { @@ -109,6 +111,8 @@ public class MpMessageSendOutReqBO {
109 * 111 *
110 * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC 112 * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
111 */ 113 */
  114 + @NotEmpty(message = "高质量音乐链接不能为空", groups = MusicMessageGroup.class)
  115 + @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
112 private String hqMusicUrl; 116 private String hqMusicUrl;
113 117
114 } 118 }
yudao-ui-admin/src/views/mp/menu/index.vue
@@ -20,6 +20,9 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER @@ -20,6 +20,9 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE. 22 SOFTWARE.
  23 + 芋道源码:
  24 + ① less 切到 scss,减少对 less 和 less-loader 的依赖
  25 + ②
23 --> 26 -->
24 <template> 27 <template>
25 <div class="app-container"> 28 <div class="app-container">
@@ -75,60 +78,64 @@ SOFTWARE. @@ -75,60 +78,64 @@ SOFTWARE.
75 </div> 78 </div>
76 <div> 79 <div>
77 <span>菜单名称:</span> 80 <span>菜单名称:</span>
78 - <el-input class="input_width" v-model="tempObj.name" placeholder="请输入菜单名称" :maxlength="nameMaxLength"  
79 - clearable /> 81 + <el-input class="input_width" v-model="tempObj.name" placeholder="请输入菜单名称" :maxlength="nameMaxLength" clearable />
80 </div> 82 </div>
81 <div v-if="showConfigureContent"> 83 <div v-if="showConfigureContent">
82 - <div class="menu_content">  
83 - <span>菜单内容:</span>  
84 - <el-select v-model="tempObj.type" clearable placeholder="请选择" class="menu_option">  
85 - <el-option v-for="item in menuOptions" :label="item.label" :value="item.value" :key="item.value" />  
86 - </el-select>  
87 - </div>  
88 - <div class="configur_content" v-if="tempObj.type === 'view'">  
89 - <span>跳转链接:</span>  
90 - <el-input class="input_width" v-model="tempObj.url" placeholder="请输入链接" clearable />  
91 - </div>  
92 - <div class="configur_content" v-if="tempObj.type === 'miniprogram'">  
93 - <div class="applet">  
94 - <span>小程序的appid:</span>  
95 - <el-input class="input_width" v-model="tempObj.appid" placeholder="请输入小程序的appid" clearable></el-input>  
96 - </div>  
97 - <div class="applet">  
98 - <span>小程序的页面路径:</span>  
99 - <el-input class="input_width" v-model="tempObj.pagepath" placeholder="请输入小程序的页面路径,如:pages/index" clearable></el-input>  
100 - </div>  
101 - <div class="applet">  
102 - <span>备用网页:</span>  
103 - <el-input class="input_width" v-model="tempObj.url" placeholder="不支持小程序的老版本客户端将打开本网页" clearable></el-input>  
104 - </div>  
105 - <p class="blue">tips:需要和公众号进行关联才可以把小程序绑定带微信菜单上哟!</p> 84 + <div class="menu_content">
  85 + <span>菜单标识:</span>
  86 + <el-input class="input_width" v-model="tempObj.menuKey" placeholder="请输入菜单 KEY" clearable />
  87 + </div>
  88 + <div class="menu_content">
  89 + <span>菜单内容:</span>
  90 + <el-select v-model="tempObj.type" clearable placeholder="请选择" class="menu_option">
  91 + <el-option v-for="item in menuOptions" :label="item.label" :value="item.value" :key="item.value" />
  92 + </el-select>
  93 + </div>
  94 + <div class="configur_content" v-if="tempObj.type === 'view'">
  95 + <span>跳转链接:</span>
  96 + <el-input class="input_width" v-model="tempObj.url" placeholder="请输入链接" clearable />
  97 + </div>
  98 + <div class="configur_content" v-if="tempObj.type === 'miniprogram'">
  99 + <div class="applet">
  100 + <span>小程序的 appid :</span>
  101 + <el-input class="input_width" v-model="tempObj.miniProgramAppId" placeholder="请输入小程序的appid" clearable />
106 </div> 102 </div>
107 - <div class="configur_content" v-if="tempObj.type === 'article_view_limited'">  
108 - <el-row>  
109 - <div class="select-item" v-if="tempObj && tempObj.content && tempObj.content.articles">  
110 - <WxNews :objData="tempObj.content.articles"></WxNews>  
111 - <el-row class="ope-row">  
112 - <el-button type="danger" icon="el-icon-delete" circle @click="deleteTempObj"></el-button>  
113 - </el-row>  
114 - </div>  
115 - <div v-if="!tempObj.content || !tempObj.content.articles">  
116 - <el-row>  
117 - <el-col :span="24" style="text-align: center">  
118 - <el-button type="success" @click="openMaterial">素材库选择<i class="el-icon-circle-check el-icon--right"></i></el-button>  
119 - </el-col>  
120 - </el-row>  
121 - </div>  
122 - <el-dialog title="选择图文" :visible.sync="dialogNewsVisible" width="90%">  
123 - <WxMaterialSelect :objData="{repType:'news'}" @selectMaterial="selectMaterial"></WxMaterialSelect>  
124 - </el-dialog>  
125 - </el-row> 103 + <div class="applet">
  104 + <span>小程序的页面路径:</span>
  105 + <el-input class="input_width" v-model="tempObj.miniProgramPagePath"
  106 + placeholder="请输入小程序的页面路径,如:pages/index" clearable />
126 </div> 107 </div>
127 - <div class="configur_content" v-if="tempObj.type === 'click' || tempObj.type === 'scancode_waitmsg'">  
128 - <WxReplySelect :objData="tempObj" v-if="hackResetWxReplySelect"></WxReplySelect> 108 + <div class="applet">
  109 + <span>小程序的备用网页:</span>
  110 + <el-input class="input_width" v-model="tempObj.url" placeholder="不支持小程序的老版本客户端将打开本网页" clearable />
129 </div> 111 </div>
130 - </div>  
131 - </div> 112 + <p class="blue">tips:需要和公众号进行关联才可以把小程序绑定带微信菜单上哟!</p>
  113 + </div>
  114 + <div class="configur_content" v-if="tempObj.type === 'article_view_limited'">
  115 + <el-row>
  116 + <div class="select-item" v-if="tempObj && tempObj.content && tempObj.content.articles">
  117 + <WxNews :objData="tempObj.content.articles"></WxNews>
  118 + <el-row class="ope-row">
  119 + <el-button type="danger" icon="el-icon-delete" circle @click="deleteTempObj"></el-button>
  120 + </el-row>
  121 + </div>
  122 + <div v-if="!tempObj.content || !tempObj.content.articles">
  123 + <el-row>
  124 + <el-col :span="24" style="text-align: center">
  125 + <el-button type="success" @click="openMaterial">素材库选择<i class="el-icon-circle-check el-icon--right"></i></el-button>
  126 + </el-col>
  127 + </el-row>
  128 + </div>
  129 + <el-dialog title="选择图文" :visible.sync="dialogNewsVisible" width="90%">
  130 + <WxMaterialSelect :objData="{repType:'news'}" @selectMaterial="selectMaterial"></WxMaterialSelect>
  131 + </el-dialog>
  132 + </el-row>
  133 + </div>
  134 + <div class="configur_content" v-if="tempObj.type === 'click' || tempObj.type === 'scancode_waitmsg'">
  135 + <wx-reply-select :objData="tempObj.reply" v-if="hackResetWxReplySelect" />
  136 + </div>
  137 + </div>
  138 + </div>
132 </div> 139 </div>
133 <!-- 一进页面就显示的默认页面,当点击左边按钮的时候,就不显示了--> 140 <!-- 一进页面就显示的默认页面,当点击左边按钮的时候,就不显示了-->
134 <div v-else class="right"> 141 <div v-else class="right">
@@ -139,10 +146,10 @@ SOFTWARE. @@ -139,10 +146,10 @@ SOFTWARE.
139 </template> 146 </template>
140 147
141 <script> 148 <script>
142 -import WxReplySelect from '@/views/mp/components/wx-news/main.vue' 149 +import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
143 import WxNews from '@/views/mp/components/wx-news/main.vue'; 150 import WxNews from '@/views/mp/components/wx-news/main.vue';
144 import WxMaterialSelect from '@/views/mp/components/wx-news/main.vue' 151 import WxMaterialSelect from '@/views/mp/components/wx-news/main.vue'
145 -import {deleteMenu, getMenuList, saveMenu} from "@/api/mp/menu"; 152 +import { deleteMenu, getMenuList, saveMenu } from "@/api/mp/menu";
146 import { getSimpleAccounts } from "@/api/mp/account"; 153 import { getSimpleAccounts } from "@/api/mp/account";
147 154
148 export default { 155 export default {
@@ -177,7 +184,7 @@ export default { @@ -177,7 +184,7 @@ export default {
177 showConfigureContent: true, // 是否展示配置内容;如果有子菜单,就不显示配置内容 184 showConfigureContent: true, // 是否展示配置内容;如果有子菜单,就不显示配置内容
178 hackResetWxReplySelect: false, // 重置 WxReplySelect 组件 185 hackResetWxReplySelect: false, // 重置 WxReplySelect 组件
179 186
180 - tempObj:{}, // 右边临时变量,作为中间值牵引关系 187 + tempObj: {}, // 右边临时变量,作为中间值牵引关系
181 tempSelfObj: { // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数 188 tempSelfObj: { // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数
182 }, 189 },
183 visible2: false, //素材内容 "选择素材"按钮弹框显示隐藏 190 visible2: false, //素材内容 "选择素材"按钮弹框显示隐藏
@@ -240,6 +247,7 @@ export default { @@ -240,6 +247,7 @@ export default {
240 getList() { 247 getList() {
241 this.loading = false; 248 this.loading = false;
242 getMenuList(this.accountId).then(response => { 249 getMenuList(this.accountId).then(response => {
  250 + response.data = this.convertMenuList(response.data);
243 this.menuList = this.handleTree(response.data, "id"); 251 this.menuList = this.handleTree(response.data, "id");
244 }).finally(() => { 252 }).finally(() => {
245 this.loading = false; 253 this.loading = false;
@@ -262,6 +270,39 @@ export default { @@ -262,6 +270,39 @@ export default {
262 } 270 }
263 this.handleQuery() 271 this.handleQuery()
264 }, 272 },
  273 + // 将后端返回的 menuList,转换成前端的 menuList
  274 + convertMenuList(list) {
  275 + const menuList = [];
  276 + list.forEach(item => {
  277 + const menu = {
  278 + ...item,
  279 + };
  280 + if (item.type === 'click' || item.type === 'scancode_waitmsg') {
  281 + this.$delete(menu, 'replyMessageType');
  282 + this.$delete(menu, 'replyContent');
  283 + this.$delete(menu, 'replyMediaId');
  284 + this.$delete(menu, 'replyMediaUrl');
  285 + this.$delete(menu, 'replyDescription');
  286 + this.$delete(menu, 'replyArticles');
  287 + menu.reply = {
  288 + type: item.replyMessageType,
  289 + accountId: item.accountId,
  290 + content: item.replyContent,
  291 + mediaId: item.replyMediaId,
  292 + url: item.replyMediaUrl,
  293 + title: item.replyTitle,
  294 + description: item.replyDescription,
  295 + thumbMediaId: item.replyThumbMediaId,
  296 + thumbMediaUrl: item.replyThumbMediaUrl,
  297 + articles: item.replyArticles,
  298 + musicUrl: item.replyMusicUrl,
  299 + hqMusicUrl: item.replyHqMusicUrl,
  300 + }
  301 + }
  302 + menuList.push(menu);
  303 + });
  304 + return menuList;
  305 + },
265 306
266 // ======================== 菜单操作 ======================== 307 // ======================== 菜单操作 ========================
267 // 一级菜单点击事件 308 // 一级菜单点击事件
@@ -285,7 +326,7 @@ export default { @@ -285,7 +326,7 @@ export default {
285 // 右侧的表单相关 326 // 右侧的表单相关
286 this.resetEditor(); 327 this.resetEditor();
287 this.showRightFlag = true; // 右边菜单 328 this.showRightFlag = true; // 右边菜单
288 - this.tempObj = subItem;//将点击的数据放到临时变量,对象有引用作用 329 + this.tempObj = subItem; // 将点击的数据放到临时变量,对象有引用作用
289 this.tempSelfObj.grand = "2"; // 表示二级菜单 330 this.tempSelfObj.grand = "2"; // 表示二级菜单
290 this.tempSelfObj.index = index; // 表示一级菜单索引 331 this.tempSelfObj.index = index; // 表示一级菜单索引
291 this.tempSelfObj.secondIndex = k; // 表示二级菜单索引 332 this.tempSelfObj.secondIndex = k; // 表示二级菜单索引
@@ -301,19 +342,27 @@ export default { @@ -301,19 +342,27 @@ export default {
301 const menuKeyLength = this.menuList.length; 342 const menuKeyLength = this.menuList.length;
302 const addButton = { 343 const addButton = {
303 name: "菜单名称", 344 name: "菜单名称",
304 - children: [] 345 + children: [],
  346 + reply: { // 用于存储回复内容
  347 + 'type': 'text',
  348 + 'accountId': this.accountId // 保证组件里,可以使用到对应的公众号
  349 + }
305 } 350 }
306 this.$set(this.menuList, menuKeyLength, addButton) 351 this.$set(this.menuList, menuKeyLength, addButton)
307 this.menuClick(this.menuKeyLength - 1, addButton) 352 this.menuClick(this.menuKeyLength - 1, addButton)
308 }, 353 },
309 // 添加横向二级菜单;item 表示要操作的父菜单 354 // 添加横向二级菜单;item 表示要操作的父菜单
310 addSubMenu(i, item) { 355 addSubMenu(i, item) {
311 - if (!item.children || item.children.length <= 0){ 356 + // 清空父菜单的属性,因为它只需要 name 属性即可
  357 + if (!item.children || item.children.length <= 0) {
312 this.$set( item, 'children',[]) 358 this.$set( item, 'children',[])
313 - // TODO 芋艿:需要搞的属性弄下  
314 this.$delete( item, 'type') 359 this.$delete( item, 'type')
315 - this.$delete( item, 'pagepath') 360 + this.$delete( item, 'miniProgramAppId')
  361 + this.$delete( item, 'miniProgramPagePath')
316 this.$delete( item, 'url') 362 this.$delete( item, 'url')
  363 + this.$delete( item, 'reply')
  364 + // TODO 芋艿:需要搞的属性弄下
  365 +
317 this.$delete( item, 'key') 366 this.$delete( item, 'key')
318 this.$delete( item, 'article_id') 367 this.$delete( item, 'article_id')
319 this.$delete( item, 'textContent') 368 this.$delete( item, 'textContent')
@@ -322,7 +371,11 @@ export default { @@ -322,7 +371,11 @@ export default {
322 371
323 let subMenuKeyLength = item.children.length; // 获取二级菜单key长度 372 let subMenuKeyLength = item.children.length; // 获取二级菜单key长度
324 let addButton = { 373 let addButton = {
325 - name: "子菜单名称" 374 + name: "子菜单名称",
  375 + reply: { // 用于存储回复内容
  376 + 'type': 'text',
  377 + 'accountId': this.accountId // 保证组件里,可以使用到对应的公众号
  378 + }
326 } 379 }
327 this.$set(item.children, subMenuKeyLength, addButton); 380 this.$set(item.children, subMenuKeyLength, addButton);
328 this.subMenuClick(item.children[subMenuKeyLength], i, subMenuKeyLength) 381 this.subMenuClick(item.children[subMenuKeyLength], i, subMenuKeyLength)
@@ -352,19 +405,19 @@ export default { @@ -352,19 +405,19 @@ export default {
352 handleSave() { 405 handleSave() {
353 this.$modal.confirm('确定要保证并发布该菜单吗?').then(() => { 406 this.$modal.confirm('确定要保证并发布该菜单吗?').then(() => {
354 this.loading = true 407 this.loading = true
355 - return saveMenu(this.accountId, this.menuList); 408 + return saveMenu(this.accountId, this.convertMenuFormList());
356 }).then(() => { 409 }).then(() => {
357 this.getList(); 410 this.getList();
358 this.$modal.msgSuccess("发布成功"); 411 this.$modal.msgSuccess("发布成功");
359 - }).catch(() => {}).finally(() => { 412 + }).finally(() => {
360 this.loading = false 413 this.loading = false
361 }); 414 });
362 }, 415 },
363 // 表单 Editor 重置 416 // 表单 Editor 重置
364 resetEditor() { 417 resetEditor() {
365 - this.hackResetEditor = false // 销毁组件 418 + this.hackResetWxReplySelect = false // 销毁组件
366 this.$nextTick(() => { 419 this.$nextTick(() => {
367 - this.hackResetEditor = true // 重建组件 420 + this.hackResetWxReplySelect = true // 重建组件
368 }) 421 })
369 }, 422 },
370 handleDelete() { 423 handleDelete() {
@@ -378,6 +431,45 @@ export default { @@ -378,6 +431,45 @@ export default {
378 this.loading = false 431 this.loading = false
379 }); 432 });
380 }, 433 },
  434 + // 将前端的 menuList,转换成后端接收的 menuList
  435 + convertMenuFormList() {
  436 + const menuList = [];
  437 + this.menuList.forEach(item => {
  438 + let menu = this.convertMenuForm(item);
  439 + menuList.push(menu);
  440 + // 处理子菜单
  441 + if (!item.children || item.children.length <= 0) {
  442 + return;
  443 + }
  444 + item.children = [];
  445 + item.children.forEach(subItem => {
  446 + menu.children.push(this.convertMenuForm(subItem))
  447 + })
  448 + })
  449 + return menuList;
  450 + },
  451 + // 将前端的 menu,转换成后端接收的 menu
  452 + convertMenuForm(menu) {
  453 + let result = {
  454 + ...menu,
  455 + children: undefined, // 不处理子节点
  456 + reply: undefined, // 稍后复制
  457 + }
  458 + if (menu.type === 'click' || menu.type === 'scancode_waitmsg') {
  459 + result.replyMessageType = menu.reply.type;
  460 + result.replyContent = menu.reply.content;
  461 + result.replyMediaId = menu.reply.mediaId;
  462 + result.replyMediaUrl = menu.reply.url;
  463 + result.replyTitle = menu.reply.title;
  464 + result.replyDescription = menu.reply.description;
  465 + result.replyThumbMediaId = menu.reply.thumbMediaId;
  466 + result.replyThumbMediaUrl = menu.reply.thumbMediaUrl;
  467 + result.replyArticles = menu.reply.articles;
  468 + result.replyMusicUrl = menu.reply.musicUrl;
  469 + result.replyHqMusicUrl = menu.reply.hqMusicUrl;
  470 + }
  471 + return result;
  472 + },
381 473
382 // TODO 芋艿:未归类 474 // TODO 芋艿:未归类
383 475