Commit 0499226c3d3f15fb8b0581b01e46d207c7a4be3f
1 parent
141e4e4c
mp:完善【菜单】的回复功能
Showing
12 changed files
with
406 additions
and
197 deletions
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java
| 1 | 1 | package cn.iocoder.yudao.module.mp.controller.admin.menu.vo; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; |
| 4 | +import io.swagger.annotations.ApiModelProperty; | |
| 4 | 5 | import lombok.Data; |
| 5 | 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 | 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 | 17 | * 微信菜单 Base VO,提供给添加、修改、详细的子 VO 使用 |
| 12 | 18 | * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 |
| ... | ... | @@ -38,70 +44,51 @@ public class MpMenuBaseVO { |
| 38 | 44 | */ |
| 39 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 89 | private String replyTitle; |
| 100 | - /** | |
| 101 | - * 回复的描述 | |
| 102 | - * | |
| 103 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO | |
| 104 | - */ | |
| 90 | + @ApiModelProperty(value = "回复的描述", example = "视频描述") | |
| 91 | + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) | |
| 105 | 92 | private String replyDescription; |
| 106 | 93 | |
| 107 | 94 | /** |
| ... | ... | @@ -109,6 +96,17 @@ public class MpMenuBaseVO { |
| 109 | 96 | * |
| 110 | 97 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS |
| 111 | 98 | */ |
| 99 | + @NotNull(message = "回复的图文消息不能为空", groups = NewsMessageGroup.class) | |
| 100 | + @Valid | |
| 112 | 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 | 26 | public String type; |
| 27 | 27 | |
| 28 | 28 | @ApiModelProperty(value = "消息内容", required = true, example = "你好呀") |
| 29 | - @NotEmpty(message = "消息内容不能为空", groups = TextGroup.class) | |
| 29 | + @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class) | |
| 30 | 30 | private String content; |
| 31 | 31 | |
| 32 | 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 | 34 | private String mediaId; |
| 35 | 35 | |
| 36 | 36 | @ApiModelProperty(value = "标题", required = true, example = "没有标题") |
| 37 | - @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) | |
| 37 | + @NotEmpty(message = "消息内容不能为空", groups = VideoMessageGroup.class) | |
| 38 | 38 | private String title; |
| 39 | 39 | |
| 40 | 40 | @ApiModelProperty(value = "描述", required = true, example = "你猜") |
| 41 | - @NotEmpty(message = "消息描述不能为空", groups = VideoGroup.class) | |
| 41 | + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) | |
| 42 | 42 | private String description; |
| 43 | 43 | |
| 44 | 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 | 46 | private String thumbMediaId; |
| 47 | 47 | |
| 48 | 48 | @ApiModelProperty(value = "图文消息", required = true) |
| 49 | 49 | @Valid |
| 50 | - @NotNull(message = "图文消息不能为空", groups = NewsGroup.class) | |
| 50 | + @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class) | |
| 51 | 51 | private List<MpMessageDO.Article> articles; |
| 52 | 52 | |
| 53 | 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 | 26 | @Mapping(source = "menu.replyMessageType", target = "type"), |
| 27 | 27 | @Mapping(source = "menu.replyContent", target = "content"), |
| 28 | 28 | @Mapping(source = "menu.replyMediaId", target = "mediaId"), |
| 29 | + @Mapping(source = "menu.replyThumbMediaId", target = "thumbMediaId"), | |
| 29 | 30 | @Mapping(source = "menu.replyTitle", target = "title"), |
| 30 | 31 | @Mapping(source = "menu.replyDescription", target = "description"), |
| 31 | 32 | @Mapping(source = "menu.replyArticles", target = "articles"), |
| 33 | + @Mapping(source = "menu.replyMusicUrl", target = "musicUrl"), | |
| 34 | + @Mapping(source = "menu.replyHqMusicUrl", target = "hqMusicUrl"), | |
| 32 | 35 | }) |
| 33 | 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 | 17 | @Mapping(source = "reply.responseMessageType", target = "type"), |
| 18 | 18 | @Mapping(source = "reply.responseContent", target = "content"), |
| 19 | 19 | @Mapping(source = "reply.responseMediaId", target = "mediaId"), |
| 20 | - @Mapping(source = "reply.responseMediaUrl", target = "mediaUrl"), | |
| 21 | 20 | @Mapping(source = "reply.responseTitle", target = "title"), |
| 22 | 21 | @Mapping(source = "reply.responseDescription", target = "description"), |
| 23 | 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 | 57 | break; |
| 58 | 58 | case WxConsts.XmlMsgType.IMAGE: // 2. 图片 |
| 59 | 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 | 63 | break; |
| 62 | 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 | 68 | .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()); |
| 65 | 69 | break; |
| 66 | 70 | case WxConsts.XmlMsgType.NEWS: // 5. 图文 |
| ... | ... | @@ -69,7 +73,7 @@ public interface MpMessageConvert { |
| 69 | 73 | message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()) |
| 70 | 74 | .setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl()) |
| 71 | 75 | .setThumbMediaId(sendReqBO.getThumbMediaId()); |
| 72 | -// .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:url 待确定 | |
| 76 | +// .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:去 url | |
| 73 | 77 | break; |
| 74 | 78 | default: |
| 75 | 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 | 103 | * |
| 104 | 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 | 108 | private String replyMessageType; |
| 109 | 109 | |
| ... | ... | @@ -141,6 +141,19 @@ public class MpMenuDO extends BaseDO { |
| 141 | 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 | 159 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS |
| ... | ... | @@ -148,4 +161,19 @@ public class MpMenuDO extends BaseDO { |
| 148 | 161 | @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) |
| 149 | 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 | 124 | /** |
| 125 | 125 | * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id |
| 126 | 126 | * |
| 127 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC | |
| 127 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO | |
| 128 | 128 | */ |
| 129 | 129 | private String thumbMediaId; |
| 130 | 130 | /** |
| 131 | 131 | * 缩略图的媒体 URL |
| 132 | 132 | * |
| 133 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO | |
| 133 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO | |
| 134 | 134 | */ |
| 135 | 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 | 1 | package cn.iocoder.yudao.module.mp.framework.mp.core.util; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; |
| 4 | 5 | import lombok.extern.slf4j.Slf4j; |
| 5 | 6 | import me.chanjar.weixin.common.api.WxConsts; |
| ... | ... | @@ -15,36 +16,6 @@ import javax.validation.Validator; |
| 15 | 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 | 21 | * @param type 类型 |
| ... | ... | @@ -55,22 +26,22 @@ public class MpUtils { |
| 55 | 26 | Class<?> group; |
| 56 | 27 | switch (type) { |
| 57 | 28 | case WxConsts.XmlMsgType.TEXT: |
| 58 | - group = TextGroup.class; | |
| 29 | + group = TextMessageGroup.class; | |
| 59 | 30 | break; |
| 60 | 31 | case WxConsts.XmlMsgType.IMAGE: |
| 61 | - group = ImageGroup.class; | |
| 32 | + group = ImageMessageGroup.class; | |
| 62 | 33 | break; |
| 63 | 34 | case WxConsts.XmlMsgType.VOICE: |
| 64 | - group = VoiceGroup.class; | |
| 35 | + group = VoiceMessageGroup.class; | |
| 65 | 36 | break; |
| 66 | 37 | case WxConsts.XmlMsgType.VIDEO: |
| 67 | - group = VideoGroup.class; | |
| 38 | + group = VideoMessageGroup.class; | |
| 68 | 39 | break; |
| 69 | 40 | case WxConsts.XmlMsgType.NEWS: |
| 70 | - group = NewsGroup.class; | |
| 41 | + group = NewsMessageGroup.class; | |
| 71 | 42 | break; |
| 72 | 43 | case WxConsts.XmlMsgType.MUSIC: |
| 73 | - group = MusicGroup.class; | |
| 44 | + group = MusicMessageGroup.class; | |
| 74 | 45 | break; |
| 75 | 46 | default: |
| 76 | 47 | log.error("[validateMessage][未知的消息类型({})]", message); |
| ... | ... | @@ -80,6 +51,35 @@ public class MpUtils { |
| 80 | 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 | 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 | 8 | import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; |
| 9 | 9 | import cn.iocoder.yudao.module.mp.dal.mysql.menu.MpMenuMapper; |
| 10 | 10 | import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; |
| 11 | +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils; | |
| 11 | 12 | import cn.iocoder.yudao.module.mp.service.account.MpAccountService; |
| 12 | 13 | import cn.iocoder.yudao.module.mp.service.message.MpMessageService; |
| 13 | 14 | import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; |
| ... | ... | @@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional; |
| 22 | 23 | import org.springframework.validation.annotation.Validated; |
| 23 | 24 | |
| 24 | 25 | import javax.annotation.Resource; |
| 26 | +import javax.validation.Validator; | |
| 25 | 27 | import java.util.List; |
| 26 | 28 | |
| 27 | 29 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| ... | ... | @@ -49,6 +51,9 @@ public class MpMenuServiceImpl implements MpMenuService { |
| 49 | 51 | private MpServiceFactory mpServiceFactory; |
| 50 | 52 | |
| 51 | 53 | @Resource |
| 54 | + private Validator validator; | |
| 55 | + | |
| 56 | + @Resource | |
| 52 | 57 | private MpMenuMapper mpMenuMapper; |
| 53 | 58 | |
| 54 | 59 | @Override |
| ... | ... | @@ -57,6 +62,9 @@ public class MpMenuServiceImpl implements MpMenuService { |
| 57 | 62 | MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); |
| 58 | 63 | WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId()); |
| 59 | 64 | |
| 65 | + // 参数校验 | |
| 66 | + createReqVO.getMenus().forEach(this::validateMenu); | |
| 67 | + | |
| 60 | 68 | // 第一步,同步公众号 |
| 61 | 69 | WxMenu wxMenu = new WxMenu(); |
| 62 | 70 | wxMenu.setButtons(MpMenuConvert.INSTANCE.convert(createReqVO.getMenus())); |
| ... | ... | @@ -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 | 112 | private MpMenuDO createMenu(MpMenuSaveReqVO.Menu wxMenu, MpMenuDO parentMenu, MpAccountDO account) { |
| 113 | + // 创建菜单 | |
| 97 | 114 | MpMenuDO menu = CollUtil.isNotEmpty(wxMenu.getChildren()) |
| 98 | 115 | ? new MpMenuDO().setName(wxMenu.getName()) |
| 99 | 116 | : MpMenuConvert.INSTANCE.convert02(wxMenu); |
| 117 | + // 设置菜单的公众号账号信息 | |
| 100 | 118 | if (account != null) { |
| 101 | 119 | menu.setAccountId(account.getId()).setAppId(account.getAppId()); |
| 102 | 120 | } |
| 121 | + // 设置父编号 | |
| 103 | 122 | if (parentMenu != null) { |
| 104 | 123 | menu.setParentId(parentMenu.getId()); |
| 105 | 124 | } else { |
| 106 | 125 | menu.setParentId(MpMenuDO.ID_ROOT); |
| 107 | 126 | } |
| 108 | - if (StrUtil.isNotEmpty(wxMenu.getReplyMediaId())) { | |
| 109 | - throw new IllegalArgumentException("未实现"); | |
| 110 | - } | |
| 127 | + | |
| 128 | + // 插入到数据库 | |
| 111 | 129 | mpMenuMapper.insert(menu); |
| 112 | 130 | return menu; |
| 113 | 131 | } |
| 114 | 132 | |
| 115 | 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 | 148 | public WxMpXmlOutMessage reply(String appId, String key, String openid) { |
| 117 | 149 | // 第一步,获得菜单 |
| 118 | 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 | 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 | 81 | downloadMessageMedia(message); |
| 82 | 82 | mpMessageMapper.insert(message); |
| 83 | 83 | } |
| ... | ... | @@ -94,9 +94,9 @@ public class MpMessageServiceImpl implements MpMessageService { |
| 94 | 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 | 100 | mpMessageMapper.insert(message); |
| 101 | 101 | |
| 102 | 102 | // 转换返回 WxMpXmlOutMessage 对象 |
| ... | ... | @@ -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 | 127 | downloadMessageMedia(message); |
| 128 | 128 | mpMessageMapper.insert(message); |
| 129 | 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 | 4 | import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*; |
| 5 | 5 | import lombok.Data; |
| 6 | 6 | import me.chanjar.weixin.common.api.WxConsts; |
| 7 | +import org.hibernate.validator.constraints.URL; | |
| 7 | 8 | |
| 8 | 9 | import javax.validation.Valid; |
| 9 | 10 | import javax.validation.constraints.NotEmpty; |
| ... | ... | @@ -45,7 +46,7 @@ public class MpMessageSendOutReqBO { |
| 45 | 46 | * |
| 46 | 47 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT |
| 47 | 48 | */ |
| 48 | - @NotEmpty(message = "消息内容不能为空", groups = TextGroup.class) | |
| 49 | + @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class) | |
| 49 | 50 | private String content; |
| 50 | 51 | |
| 51 | 52 | /** |
| ... | ... | @@ -53,39 +54,38 @@ public class MpMessageSendOutReqBO { |
| 53 | 54 | * |
| 54 | 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 | 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 | 69 | * 缩略图的媒体 id |
| 69 | 70 | * |
| 70 | 71 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC |
| 71 | 72 | */ |
| 72 | - @NotEmpty(message = "消息内容不能为空", groups = {MusicGroup.class}) | |
| 73 | + @NotEmpty(message = "消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class}) | |
| 73 | 74 | private String thumbMediaId; |
| 74 | - // TODO 芋艿:考虑去掉 | |
| 75 | 75 | |
| 76 | 76 | /** |
| 77 | 77 | * 标题 |
| 78 | 78 | * |
| 79 | 79 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO |
| 80 | 80 | */ |
| 81 | - @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) | |
| 81 | + @NotEmpty(message = "消息标题不能为空", groups = VideoMessageGroup.class) | |
| 82 | 82 | private String title; |
| 83 | 83 | /** |
| 84 | 84 | * 描述 |
| 85 | 85 | * |
| 86 | 86 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO |
| 87 | 87 | */ |
| 88 | - @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) | |
| 88 | + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) | |
| 89 | 89 | private String description; |
| 90 | 90 | |
| 91 | 91 | /** |
| ... | ... | @@ -94,7 +94,7 @@ public class MpMessageSendOutReqBO { |
| 94 | 94 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS |
| 95 | 95 | */ |
| 96 | 96 | @Valid |
| 97 | - @NotNull(message = "图文消息不能为空", groups = NewsGroup.class) | |
| 97 | + @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class) | |
| 98 | 98 | private List<MpMessageDO.Article> articles; |
| 99 | 99 | |
| 100 | 100 | /** |
| ... | ... | @@ -102,6 +102,8 @@ public class MpMessageSendOutReqBO { |
| 102 | 102 | * |
| 103 | 103 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC |
| 104 | 104 | */ |
| 105 | + @NotEmpty(message = "音乐链接不能为空", groups = MusicMessageGroup.class) | |
| 106 | + @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class) | |
| 105 | 107 | private String musicUrl; |
| 106 | 108 | |
| 107 | 109 | /** |
| ... | ... | @@ -109,6 +111,8 @@ public class MpMessageSendOutReqBO { |
| 109 | 111 | * |
| 110 | 112 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC |
| 111 | 113 | */ |
| 114 | + @NotEmpty(message = "高质量音乐链接不能为空", groups = MusicMessageGroup.class) | |
| 115 | + @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class) | |
| 112 | 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 | 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 21 | 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 22 | 22 | SOFTWARE. |
| 23 | + 芋道源码: | |
| 24 | + ① less 切到 scss,减少对 less 和 less-loader 的依赖 | |
| 25 | + ② | |
| 23 | 26 | --> |
| 24 | 27 | <template> |
| 25 | 28 | <div class="app-container"> |
| ... | ... | @@ -75,60 +78,64 @@ SOFTWARE. |
| 75 | 78 | </div> |
| 76 | 79 | <div> |
| 77 | 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 | 82 | </div> |
| 81 | 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 | 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 | 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 | 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 | 139 | </div> |
| 133 | 140 | <!-- 一进页面就显示的默认页面,当点击左边按钮的时候,就不显示了--> |
| 134 | 141 | <div v-else class="right"> |
| ... | ... | @@ -139,10 +146,10 @@ SOFTWARE. |
| 139 | 146 | </template> |
| 140 | 147 | |
| 141 | 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 | 150 | import WxNews from '@/views/mp/components/wx-news/main.vue'; |
| 144 | 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 | 153 | import { getSimpleAccounts } from "@/api/mp/account"; |
| 147 | 154 | |
| 148 | 155 | export default { |
| ... | ... | @@ -177,7 +184,7 @@ export default { |
| 177 | 184 | showConfigureContent: true, // 是否展示配置内容;如果有子菜单,就不显示配置内容 |
| 178 | 185 | hackResetWxReplySelect: false, // 重置 WxReplySelect 组件 |
| 179 | 186 | |
| 180 | - tempObj:{}, // 右边临时变量,作为中间值牵引关系 | |
| 187 | + tempObj: {}, // 右边临时变量,作为中间值牵引关系 | |
| 181 | 188 | tempSelfObj: { // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数 |
| 182 | 189 | }, |
| 183 | 190 | visible2: false, //素材内容 "选择素材"按钮弹框显示隐藏 |
| ... | ... | @@ -240,6 +247,7 @@ export default { |
| 240 | 247 | getList() { |
| 241 | 248 | this.loading = false; |
| 242 | 249 | getMenuList(this.accountId).then(response => { |
| 250 | + response.data = this.convertMenuList(response.data); | |
| 243 | 251 | this.menuList = this.handleTree(response.data, "id"); |
| 244 | 252 | }).finally(() => { |
| 245 | 253 | this.loading = false; |
| ... | ... | @@ -262,6 +270,39 @@ export default { |
| 262 | 270 | } |
| 263 | 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 | 326 | // 右侧的表单相关 |
| 286 | 327 | this.resetEditor(); |
| 287 | 328 | this.showRightFlag = true; // 右边菜单 |
| 288 | - this.tempObj = subItem;//将点击的数据放到临时变量,对象有引用作用 | |
| 329 | + this.tempObj = subItem; // 将点击的数据放到临时变量,对象有引用作用 | |
| 289 | 330 | this.tempSelfObj.grand = "2"; // 表示二级菜单 |
| 290 | 331 | this.tempSelfObj.index = index; // 表示一级菜单索引 |
| 291 | 332 | this.tempSelfObj.secondIndex = k; // 表示二级菜单索引 |
| ... | ... | @@ -301,19 +342,27 @@ export default { |
| 301 | 342 | const menuKeyLength = this.menuList.length; |
| 302 | 343 | const addButton = { |
| 303 | 344 | name: "菜单名称", |
| 304 | - children: [] | |
| 345 | + children: [], | |
| 346 | + reply: { // 用于存储回复内容 | |
| 347 | + 'type': 'text', | |
| 348 | + 'accountId': this.accountId // 保证组件里,可以使用到对应的公众号 | |
| 349 | + } | |
| 305 | 350 | } |
| 306 | 351 | this.$set(this.menuList, menuKeyLength, addButton) |
| 307 | 352 | this.menuClick(this.menuKeyLength - 1, addButton) |
| 308 | 353 | }, |
| 309 | 354 | // 添加横向二级菜单;item 表示要操作的父菜单 |
| 310 | 355 | addSubMenu(i, item) { |
| 311 | - if (!item.children || item.children.length <= 0){ | |
| 356 | + // 清空父菜单的属性,因为它只需要 name 属性即可 | |
| 357 | + if (!item.children || item.children.length <= 0) { | |
| 312 | 358 | this.$set( item, 'children',[]) |
| 313 | - // TODO 芋艿:需要搞的属性弄下 | |
| 314 | 359 | this.$delete( item, 'type') |
| 315 | - this.$delete( item, 'pagepath') | |
| 360 | + this.$delete( item, 'miniProgramAppId') | |
| 361 | + this.$delete( item, 'miniProgramPagePath') | |
| 316 | 362 | this.$delete( item, 'url') |
| 363 | + this.$delete( item, 'reply') | |
| 364 | + // TODO 芋艿:需要搞的属性弄下 | |
| 365 | + | |
| 317 | 366 | this.$delete( item, 'key') |
| 318 | 367 | this.$delete( item, 'article_id') |
| 319 | 368 | this.$delete( item, 'textContent') |
| ... | ... | @@ -322,7 +371,11 @@ export default { |
| 322 | 371 | |
| 323 | 372 | let subMenuKeyLength = item.children.length; // 获取二级菜单key长度 |
| 324 | 373 | let addButton = { |
| 325 | - name: "子菜单名称" | |
| 374 | + name: "子菜单名称", | |
| 375 | + reply: { // 用于存储回复内容 | |
| 376 | + 'type': 'text', | |
| 377 | + 'accountId': this.accountId // 保证组件里,可以使用到对应的公众号 | |
| 378 | + } | |
| 326 | 379 | } |
| 327 | 380 | this.$set(item.children, subMenuKeyLength, addButton); |
| 328 | 381 | this.subMenuClick(item.children[subMenuKeyLength], i, subMenuKeyLength) |
| ... | ... | @@ -352,19 +405,19 @@ export default { |
| 352 | 405 | handleSave() { |
| 353 | 406 | this.$modal.confirm('确定要保证并发布该菜单吗?').then(() => { |
| 354 | 407 | this.loading = true |
| 355 | - return saveMenu(this.accountId, this.menuList); | |
| 408 | + return saveMenu(this.accountId, this.convertMenuFormList()); | |
| 356 | 409 | }).then(() => { |
| 357 | 410 | this.getList(); |
| 358 | 411 | this.$modal.msgSuccess("发布成功"); |
| 359 | - }).catch(() => {}).finally(() => { | |
| 412 | + }).finally(() => { | |
| 360 | 413 | this.loading = false |
| 361 | 414 | }); |
| 362 | 415 | }, |
| 363 | 416 | // 表单 Editor 重置 |
| 364 | 417 | resetEditor() { |
| 365 | - this.hackResetEditor = false // 销毁组件 | |
| 418 | + this.hackResetWxReplySelect = false // 销毁组件 | |
| 366 | 419 | this.$nextTick(() => { |
| 367 | - this.hackResetEditor = true // 重建组件 | |
| 420 | + this.hackResetWxReplySelect = true // 重建组件 | |
| 368 | 421 | }) |
| 369 | 422 | }, |
| 370 | 423 | handleDelete() { |
| ... | ... | @@ -378,6 +431,45 @@ export default { |
| 378 | 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 | 474 | // TODO 芋艿:未归类 |
| 383 | 475 | ... | ... |