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

pull/2/head
YunaiV 2023-01-15 18:28:08 +08:00
parent 141e4e4c8b
commit 0499226c3d
12 changed files with 411 additions and 202 deletions

View File

@ -1,12 +1,18 @@
package cn.iocoder.yudao.module.mp.controller.admin.menu.vo; package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.api.WxConsts;
import org.hibernate.validator.constraints.URL;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
// TODO 芋艿:完善 swagger 注解 import static cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
/** /**
* Base VO VO 使 * Base VO VO 使
* VO Swagger * VO Swagger
@ -38,70 +44,51 @@ public class MpMenuBaseVO {
*/ */
private String type; private String type;
/** @ApiModelProperty(value = "网页链接", example = "https://www.iocoder.cn/")
* @NotEmpty(message = "网页链接不能为空", groups = {ViewButtonGroup.class, MiniProgramButtonGroup.class})
* @URL(message = "网页链接必须是 URL 格式")
* 1024
*
* {@link WxConsts.XmlMsgType} VIEWMINIPROGRAM
*/
private String url; private String url;
/** @ApiModelProperty(value = "小程序的 appId", example = "wx1234567890")
* appId @NotEmpty(message = "小程序的 appId 不能为空", groups = MiniProgramButtonGroup.class)
*
* {@link WxConsts.MenuButtonType} MINIPROGRAM
*/
private String miniProgramAppId; private String miniProgramAppId;
/**
* @ApiModelProperty(value = "小程序的页面路径", example = "pages/index/index")
* @NotEmpty(message = "小程序的页面路径不能为空", groups = MiniProgramButtonGroup.class)
* {@link WxConsts.MenuButtonType} MINIPROGRAM
*/
private String miniProgramPagePath; private String miniProgramPagePath;
// ========== 消息内容 ========== // ========== 消息内容 ==========
/** @ApiModelProperty(value = "消息类型", example = "text",
* notes = "枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC")
* @NotEmpty(message = "消息类型不能为空", groups = {ClickButtonGroup.class, ScanCodeWaitMsgButtonGroup.class})
* {@link #type} CLICKSCANCODE_WAITMSG
*
* {@link WxConsts.XmlMsgType} TEXTIMAGEVOICEVIDEONEWS
*/
private String replyMessageType; private String replyMessageType;
/** @ApiModelProperty(value = "回复的消息内容", example = "欢迎关注")
* @NotEmpty(message = "回复的消息内容不能为空", groups = {TextMessageGroup.class})
*
* {@link WxConsts.XmlMsgType} TEXT
*/
private String replyContent; private String replyContent;
/** @ApiModelProperty(value = "回复的媒体 id", example = "123456")
* id @NotEmpty(message = "回复的消息 mediaId 不能为空",
* groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
* {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
private String replyMediaId; private String replyMediaId;
/** @ApiModelProperty(value = "回复的媒体 URL", example = "https://www.iocoder.cn/xxx.jpg")
* URL @NotEmpty(message = "回复的消息 mediaId 不能为空",
* groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
* {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
private String replyMediaUrl; private String replyMediaUrl;
/** @ApiModelProperty(value = "缩略图的媒体 id", example = "123456")
* @NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class})
* private String replyThumbMediaId;
* {@link WxConsts.XmlMsgType} VIDEO @ApiModelProperty(value = "缩略图的媒体 URL",example = "https://www.iocoder.cn/xxx.jpg")
*/ @NotEmpty(message = "回复的消息 thumbMedia 地址不能为空", groups = {MusicMessageGroup.class})
private String replyThumbMediaUrl;
@ApiModelProperty(value = "回复的标题", example = "视频标题")
@NotEmpty(message = "回复的消息标题不能为空", groups = VideoMessageGroup.class)
private String replyTitle; private String replyTitle;
/** @ApiModelProperty(value = "回复的描述", example = "视频描述")
* @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
*
* {@link WxConsts.XmlMsgType} VIDEO
*/
private String replyDescription; private String replyDescription;
/** /**
@ -109,6 +96,17 @@ public class MpMenuBaseVO {
* *
* {@link WxConsts.XmlMsgType} NEWS * {@link WxConsts.XmlMsgType} NEWS
*/ */
@NotNull(message = "回复的图文消息不能为空", groups = NewsMessageGroup.class)
@Valid
private List<MpMessageDO.Article> replyArticles; private List<MpMessageDO.Article> replyArticles;
@ApiModelProperty(value = "音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
@NotEmpty(message = "回复的音乐链接不能为空", groups = MusicMessageGroup.class)
@URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
private String replyMusicUrl;
@ApiModelProperty(value = "高质量音乐链接", example = "https://www.iocoder.cn/xxx.mp3")
@NotEmpty(message = "回复的高质量音乐链接不能为空", groups = MusicMessageGroup.class)
@URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
private String replyHqMusicUrl;
} }

View File

@ -26,28 +26,28 @@ public class MpMessageSendReqVO {
public String type; public String type;
@ApiModelProperty(value = "消息内容", required = true, example = "你好呀") @ApiModelProperty(value = "消息内容", required = true, example = "你好呀")
@NotEmpty(message = "消息内容不能为空", groups = TextGroup.class) @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class)
private String content; private String content;
@ApiModelProperty(value = "媒体 ID", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP") @ApiModelProperty(value = "媒体 ID", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
@NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) @NotEmpty(message = "消息内容不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
private String mediaId; private String mediaId;
@ApiModelProperty(value = "标题", required = true, example = "没有标题") @ApiModelProperty(value = "标题", required = true, example = "没有标题")
@NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) @NotEmpty(message = "消息内容不能为空", groups = VideoMessageGroup.class)
private String title; private String title;
@ApiModelProperty(value = "描述", required = true, example = "你猜") @ApiModelProperty(value = "描述", required = true, example = "你猜")
@NotEmpty(message = "消息描述不能为空", groups = VideoGroup.class) @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
private String description; private String description;
@ApiModelProperty(value = "缩略图的媒体 id", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP") @ApiModelProperty(value = "缩略图的媒体 id", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
@NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicGroup.class) @NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicMessageGroup.class)
private String thumbMediaId; private String thumbMediaId;
@ApiModelProperty(value = "图文消息", required = true) @ApiModelProperty(value = "图文消息", required = true)
@Valid @Valid
@NotNull(message = "图文消息不能为空", groups = NewsGroup.class) @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class)
private List<MpMessageDO.Article> articles; private List<MpMessageDO.Article> articles;
@ApiModelProperty(value = "音乐链接", example = "https://www.iocoder.cn/music.mp3", notes = "消息类型为 MUSIC 时") @ApiModelProperty(value = "音乐链接", example = "https://www.iocoder.cn/music.mp3", notes = "消息类型为 MUSIC 时")

View File

@ -26,9 +26,12 @@ public interface MpMenuConvert {
@Mapping(source = "menu.replyMessageType", target = "type"), @Mapping(source = "menu.replyMessageType", target = "type"),
@Mapping(source = "menu.replyContent", target = "content"), @Mapping(source = "menu.replyContent", target = "content"),
@Mapping(source = "menu.replyMediaId", target = "mediaId"), @Mapping(source = "menu.replyMediaId", target = "mediaId"),
@Mapping(source = "menu.replyThumbMediaId", target = "thumbMediaId"),
@Mapping(source = "menu.replyTitle", target = "title"), @Mapping(source = "menu.replyTitle", target = "title"),
@Mapping(source = "menu.replyDescription", target = "description"), @Mapping(source = "menu.replyDescription", target = "description"),
@Mapping(source = "menu.replyArticles", target = "articles"), @Mapping(source = "menu.replyArticles", target = "articles"),
@Mapping(source = "menu.replyMusicUrl", target = "musicUrl"),
@Mapping(source = "menu.replyHqMusicUrl", target = "hqMusicUrl"),
}) })
MpMessageSendOutReqBO convert(String openid, MpMenuDO menu); MpMessageSendOutReqBO convert(String openid, MpMenuDO menu);

View File

@ -17,7 +17,6 @@ public interface MpAutoReplyConvert {
@Mapping(source = "reply.responseMessageType", target = "type"), @Mapping(source = "reply.responseMessageType", target = "type"),
@Mapping(source = "reply.responseContent", target = "content"), @Mapping(source = "reply.responseContent", target = "content"),
@Mapping(source = "reply.responseMediaId", target = "mediaId"), @Mapping(source = "reply.responseMediaId", target = "mediaId"),
@Mapping(source = "reply.responseMediaUrl", target = "mediaUrl"),
@Mapping(source = "reply.responseTitle", target = "title"), @Mapping(source = "reply.responseTitle", target = "title"),
@Mapping(source = "reply.responseDescription", target = "description"), @Mapping(source = "reply.responseDescription", target = "description"),
@Mapping(source = "reply.responseArticles", target = "articles"), @Mapping(source = "reply.responseArticles", target = "articles"),

View File

@ -57,10 +57,14 @@ public interface MpMessageConvert {
break; break;
case WxConsts.XmlMsgType.IMAGE: // 2. 图片 case WxConsts.XmlMsgType.IMAGE: // 2. 图片
case WxConsts.XmlMsgType.VOICE: // 3. 语音 case WxConsts.XmlMsgType.VOICE: // 3. 语音
message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl()); message.setMediaId(sendReqBO.getMediaId())
// .setMediaUrl(sendReqBO.getMediaUrl()) TODO 芋艿:去 url
;
break; break;
case WxConsts.XmlMsgType.VIDEO: // 4. 视频 case WxConsts.XmlMsgType.VIDEO: // 4. 视频
message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl()) message.setMediaId(sendReqBO.getMediaId())
// .setMediaUrl(sendReqBO.getMediaUrl()) TODO 芋艿:去 url
.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()); .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription());
break; break;
case WxConsts.XmlMsgType.NEWS: // 5. 图文 case WxConsts.XmlMsgType.NEWS: // 5. 图文
@ -69,7 +73,7 @@ public interface MpMessageConvert {
message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()) message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription())
.setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl()) .setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl())
.setThumbMediaId(sendReqBO.getThumbMediaId()); .setThumbMediaId(sendReqBO.getThumbMediaId());
// .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:url 待确定 // .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:url
break; break;
default: default:
throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); throw new IllegalArgumentException("不支持的消息类型:" + message.getType());

View File

@ -103,7 +103,7 @@ public class MpMenuDO extends BaseDO {
* *
* {@link #type} CLICKSCANCODE_WAITMSG * {@link #type} CLICKSCANCODE_WAITMSG
* *
* {@link WxConsts.XmlMsgType} TEXTIMAGEVOICEVIDEONEWS * {@link WxConsts.XmlMsgType} TEXTIMAGEVOICEVIDEONEWSMUSIC
*/ */
private String replyMessageType; private String replyMessageType;
@ -140,6 +140,19 @@ public class MpMenuDO extends BaseDO {
*/ */
private String replyDescription; private String replyDescription;
/**
* id id
*
* {@link WxConsts.XmlMsgType} MUSICVIDEO
*/
private String replyThumbMediaId;
/**
* URL
*
* {@link WxConsts.XmlMsgType} MUSICVIDEO
*/
private String replyThumbMediaUrl;
/** /**
* *
* *
@ -148,4 +161,19 @@ public class MpMenuDO extends BaseDO {
@TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class)
private List<MpMessageDO.Article> replyArticles; private List<MpMessageDO.Article> replyArticles;
/**
*
*
* {@link WxConsts.XmlMsgType} MUSIC
*/
private String replyMusicUrl;
/**
*
*
* WIFI 使
*
* {@link WxConsts.XmlMsgType} MUSIC
*/
private String replyHqMusicUrl;
} }

View File

@ -124,13 +124,13 @@ public class MpMessageDO extends BaseDO {
/** /**
* id id * id id
* *
* {@link WxConsts.XmlMsgType} MUSIC * {@link WxConsts.XmlMsgType} MUSICVIDEO
*/ */
private String thumbMediaId; private String thumbMediaId;
/** /**
* URL * URL
* *
* {@link WxConsts.XmlMsgType} VIDEO * {@link WxConsts.XmlMsgType} MUSICVIDEO
*/ */
private String thumbMediaUrl; private String thumbMediaUrl;

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.mp.framework.mp.core.util; package cn.iocoder.yudao.module.mp.framework.mp.core.util;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.api.WxConsts;
@ -14,36 +15,6 @@ import javax.validation.Validator;
@Slf4j @Slf4j
public class MpUtils { public class MpUtils {
/**
* Text Group
*/
public interface TextGroup {}
/**
* Image Group
*/
public interface ImageGroup {}
/**
* Voice Group
*/
public interface VoiceGroup {}
/**
* Video Group
*/
public interface VideoGroup {}
/**
* News Group
*/
public interface NewsGroup {}
/**
* Music Group
*/
public interface MusicGroup {}
/** /**
* *
* *
@ -55,22 +26,22 @@ public class MpUtils {
Class<?> group; Class<?> group;
switch (type) { switch (type) {
case WxConsts.XmlMsgType.TEXT: case WxConsts.XmlMsgType.TEXT:
group = TextGroup.class; group = TextMessageGroup.class;
break; break;
case WxConsts.XmlMsgType.IMAGE: case WxConsts.XmlMsgType.IMAGE:
group = ImageGroup.class; group = ImageMessageGroup.class;
break; break;
case WxConsts.XmlMsgType.VOICE: case WxConsts.XmlMsgType.VOICE:
group = VoiceGroup.class; group = VoiceMessageGroup.class;
break; break;
case WxConsts.XmlMsgType.VIDEO: case WxConsts.XmlMsgType.VIDEO:
group = VideoGroup.class; group = VideoMessageGroup.class;
break; break;
case WxConsts.XmlMsgType.NEWS: case WxConsts.XmlMsgType.NEWS:
group = NewsGroup.class; group = NewsMessageGroup.class;
break; break;
case WxConsts.XmlMsgType.MUSIC: case WxConsts.XmlMsgType.MUSIC:
group = MusicGroup.class; group = MusicMessageGroup.class;
break; break;
default: default:
log.error("[validateMessage][未知的消息类型({})]", message); log.error("[validateMessage][未知的消息类型({})]", message);
@ -80,6 +51,35 @@ public class MpUtils {
ValidationUtils.validate(validator, message, group); ValidationUtils.validate(validator, message, group);
} }
public static void validateButton(Validator validator, String type, String messageType, Object button) {
if (StrUtil.isBlank(type)) {
return;
}
// 获得对应的校验 group
Class<?> group;
switch (type) {
case WxConsts.MenuButtonType.CLICK:
group = ClickButtonGroup.class;
validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式
break;
case WxConsts.MenuButtonType.VIEW:
group = ViewButtonGroup.class;
break;
case WxConsts.MenuButtonType.MINIPROGRAM:
group = MiniProgramButtonGroup.class;
break;
case WxConsts.MenuButtonType.SCANCODE_WAITMSG:
group = ScanCodeWaitMsgButtonGroup.class;
validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式
break;
default:
log.error("[validateButton][未知的按钮({})]", button);
throw new IllegalArgumentException("不支持的按钮类型:" + type);
}
// 执行校验
ValidationUtils.validate(validator, button, group);
}
/** /**
* *
* *
@ -101,4 +101,53 @@ public class MpUtils {
} }
} }
/**
* Text Group
*/
public interface TextMessageGroup {}
/**
* Image Group
*/
public interface ImageMessageGroup {}
/**
* Voice Group
*/
public interface VoiceMessageGroup {}
/**
* Video Group
*/
public interface VideoMessageGroup {}
/**
* News Group
*/
public interface NewsMessageGroup {}
/**
* Music Group
*/
public interface MusicMessageGroup {}
/**
* Click Group
*/
public interface ClickButtonGroup {}
/**
* View Group
*/
public interface ViewButtonGroup {}
/**
* MiniProgram Group
*/
public interface MiniProgramButtonGroup {}
/**
* SCANCODE_WAITMSG Group
*/
public interface ScanCodeWaitMsgButtonGroup {}
} }

View File

@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
import cn.iocoder.yudao.module.mp.dal.mysql.menu.MpMenuMapper; import cn.iocoder.yudao.module.mp.dal.mysql.menu.MpMenuMapper;
import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils;
import cn.iocoder.yudao.module.mp.service.account.MpAccountService; import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
import cn.iocoder.yudao.module.mp.service.message.MpMessageService; import cn.iocoder.yudao.module.mp.service.message.MpMessageService;
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Validator;
import java.util.List; import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -48,6 +50,9 @@ public class MpMenuServiceImpl implements MpMenuService {
@Lazy // 延迟加载,避免循环引用报错 @Lazy // 延迟加载,避免循环引用报错
private MpServiceFactory mpServiceFactory; private MpServiceFactory mpServiceFactory;
@Resource
private Validator validator;
@Resource @Resource
private MpMenuMapper mpMenuMapper; private MpMenuMapper mpMenuMapper;
@ -57,6 +62,9 @@ public class MpMenuServiceImpl implements MpMenuService {
MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId());
WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId()); WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId());
// 参数校验
createReqVO.getMenus().forEach(this::validateMenu);
// 第一步,同步公众号 // 第一步,同步公众号
WxMenu wxMenu = new WxMenu(); WxMenu wxMenu = new WxMenu();
wxMenu.setButtons(MpMenuConvert.INSTANCE.convert(createReqVO.getMenus())); wxMenu.setButtons(MpMenuConvert.INSTANCE.convert(createReqVO.getMenus()));
@ -79,6 +87,49 @@ public class MpMenuServiceImpl implements MpMenuService {
}); });
} }
/**
*
*
* @param menu
*/
private void validateMenu(MpMenuSaveReqVO.Menu menu) {
MpUtils.validateButton(validator, menu.getType(), menu.getReplyMessageType(), menu);
// 子菜单
if (CollUtil.isEmpty(menu.getChildren())) {
return;
}
menu.getChildren().forEach(this::validateMenu);
}
/**
*
*
* @param wxMenu
* @param parentMenu
* @param account
* @return
*/
private MpMenuDO createMenu(MpMenuSaveReqVO.Menu wxMenu, MpMenuDO parentMenu, MpAccountDO account) {
// 创建菜单
MpMenuDO menu = CollUtil.isNotEmpty(wxMenu.getChildren())
? new MpMenuDO().setName(wxMenu.getName())
: MpMenuConvert.INSTANCE.convert02(wxMenu);
// 设置菜单的公众号账号信息
if (account != null) {
menu.setAccountId(account.getId()).setAppId(account.getAppId());
}
// 设置父编号
if (parentMenu != null) {
menu.setParentId(parentMenu.getId());
} else {
menu.setParentId(MpMenuDO.ID_ROOT);
}
// 插入到数据库
mpMenuMapper.insert(menu);
return menu;
}
@Override @Override
public void deleteMenuByAccountId(Long accountId) { public void deleteMenuByAccountId(Long accountId) {
WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
@ -93,25 +144,6 @@ public class MpMenuServiceImpl implements MpMenuService {
mpMenuMapper.deleteByAccountId(accountId); mpMenuMapper.deleteByAccountId(accountId);
} }
private MpMenuDO createMenu(MpMenuSaveReqVO.Menu wxMenu, MpMenuDO parentMenu, MpAccountDO account) {
MpMenuDO menu = CollUtil.isNotEmpty(wxMenu.getChildren())
? new MpMenuDO().setName(wxMenu.getName())
: MpMenuConvert.INSTANCE.convert02(wxMenu);
if (account != null) {
menu.setAccountId(account.getId()).setAppId(account.getAppId());
}
if (parentMenu != null) {
menu.setParentId(parentMenu.getId());
} else {
menu.setParentId(MpMenuDO.ID_ROOT);
}
if (StrUtil.isNotEmpty(wxMenu.getReplyMediaId())) {
throw new IllegalArgumentException("未实现");
}
mpMenuMapper.insert(menu);
return menu;
}
@Override @Override
public WxMpXmlOutMessage reply(String appId, String key, String openid) { public WxMpXmlOutMessage reply(String appId, String key, String openid) {
// 第一步,获得菜单 // 第一步,获得菜单

View File

@ -76,8 +76,8 @@ public class MpMessageServiceImpl implements MpMessageService {
Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser()); Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser());
// 记录消息 // 记录消息
MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user); MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user)
message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom()); .setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
downloadMessageMedia(message); downloadMessageMedia(message);
mpMessageMapper.insert(message); mpMessageMapper.insert(message);
} }
@ -94,9 +94,9 @@ public class MpMessageServiceImpl implements MpMessageService {
Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid()); Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid());
// 记录消息 // 记录消息
MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user); MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user).
message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
// TODO 芋艿downloadMessageMedia downloadMessageMedia(message);
mpMessageMapper.insert(message); mpMessageMapper.insert(message);
// 转换返回 WxMpXmlOutMessage 对象 // 转换返回 WxMpXmlOutMessage 对象
@ -122,8 +122,8 @@ public class MpMessageServiceImpl implements MpMessageService {
} }
// 记录消息 // 记录消息
MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user); MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user)
message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); .setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
downloadMessageMedia(message); downloadMessageMedia(message);
mpMessageMapper.insert(message); mpMessageMapper.insert(message);
return message; return message;

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*; import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
import lombok.Data; import lombok.Data;
import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.api.WxConsts;
import org.hibernate.validator.constraints.URL;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
@ -45,7 +46,7 @@ public class MpMessageSendOutReqBO {
* *
* {@link WxConsts.XmlMsgType} TEXT * {@link WxConsts.XmlMsgType} TEXT
*/ */
@NotEmpty(message = "消息内容不能为空", groups = TextGroup.class) @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class)
private String content; private String content;
/** /**
@ -53,39 +54,38 @@ public class MpMessageSendOutReqBO {
* *
* {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO * {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/ */
@NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) @NotEmpty(message = "消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
private String mediaId; private String mediaId;
// TODO 芋艿:考虑去掉 // // TODO 芋艿:考虑去掉
/** // /**
* URL // * 媒体 URL
* // *
* {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO // * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
*/ // */
@NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) // @NotEmpty(message = "消息内容不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
private String mediaUrl; // private String mediaUrl;
/** /**
* id * id
* *
* {@link WxConsts.XmlMsgType} VIDEOMUSIC * {@link WxConsts.XmlMsgType} VIDEOMUSIC
*/ */
@NotEmpty(message = "消息内容不能为空", groups = {MusicGroup.class}) @NotEmpty(message = "消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class})
private String thumbMediaId; private String thumbMediaId;
// TODO 芋艿:考虑去掉
/** /**
* *
* *
* {@link WxConsts.XmlMsgType} VIDEO * {@link WxConsts.XmlMsgType} VIDEO
*/ */
@NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) @NotEmpty(message = "消息标题不能为空", groups = VideoMessageGroup.class)
private String title; private String title;
/** /**
* *
* *
* {@link WxConsts.XmlMsgType} VIDEO * {@link WxConsts.XmlMsgType} VIDEO
*/ */
@NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class) @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
private String description; private String description;
/** /**
@ -94,7 +94,7 @@ public class MpMessageSendOutReqBO {
* {@link WxConsts.XmlMsgType} NEWS * {@link WxConsts.XmlMsgType} NEWS
*/ */
@Valid @Valid
@NotNull(message = "图文消息不能为空", groups = NewsGroup.class) @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class)
private List<MpMessageDO.Article> articles; private List<MpMessageDO.Article> articles;
/** /**
@ -102,6 +102,8 @@ public class MpMessageSendOutReqBO {
* *
* {@link WxConsts.XmlMsgType} MUSIC * {@link WxConsts.XmlMsgType} MUSIC
*/ */
@NotEmpty(message = "音乐链接不能为空", groups = MusicMessageGroup.class)
@URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
private String musicUrl; private String musicUrl;
/** /**
@ -109,6 +111,8 @@ public class MpMessageSendOutReqBO {
* *
* {@link WxConsts.XmlMsgType} MUSIC * {@link WxConsts.XmlMsgType} MUSIC
*/ */
@NotEmpty(message = "高质量音乐链接不能为空", groups = MusicMessageGroup.class)
@URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
private String hqMusicUrl; private String hqMusicUrl;
} }

View File

@ -20,6 +20,9 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
芋道源码
less 切到 scss减少对 less less-loader 的依赖
--> -->
<template> <template>
<div class="app-container"> <div class="app-container">
@ -75,60 +78,64 @@ SOFTWARE.
</div> </div>
<div> <div>
<span>菜单名称</span> <span>菜单名称</span>
<el-input class="input_width" v-model="tempObj.name" placeholder="请输入菜单名称" :maxlength="nameMaxLength" <el-input class="input_width" v-model="tempObj.name" placeholder="请输入菜单名称" :maxlength="nameMaxLength" clearable />
clearable />
</div> </div>
<div v-if="showConfigureContent"> <div v-if="showConfigureContent">
<div class="menu_content"> <div class="menu_content">
<span>菜单内容</span> <span>菜单标识</span>
<el-select v-model="tempObj.type" clearable placeholder="请选择" class="menu_option"> <el-input class="input_width" v-model="tempObj.menuKey" placeholder="请输入菜单 KEY" clearable />
<el-option v-for="item in menuOptions" :label="item.label" :value="item.value" :key="item.value" /> </div>
</el-select> <div class="menu_content">
<span>菜单内容</span>
<el-select v-model="tempObj.type" clearable placeholder="请选择" class="menu_option">
<el-option v-for="item in menuOptions" :label="item.label" :value="item.value" :key="item.value" />
</el-select>
</div>
<div class="configur_content" v-if="tempObj.type === 'view'">
<span>跳转链接</span>
<el-input class="input_width" v-model="tempObj.url" placeholder="请输入链接" clearable />
</div>
<div class="configur_content" v-if="tempObj.type === 'miniprogram'">
<div class="applet">
<span>小程序的 appid </span>
<el-input class="input_width" v-model="tempObj.miniProgramAppId" placeholder="请输入小程序的appid" clearable />
</div> </div>
<div class="configur_content" v-if="tempObj.type === 'view'"> <div class="applet">
<span>跳转链接</span> <span>小程序的页面路径</span>
<el-input class="input_width" v-model="tempObj.url" placeholder="请输入链接" clearable /> <el-input class="input_width" v-model="tempObj.miniProgramPagePath"
placeholder="请输入小程序的页面路径pages/index" clearable />
</div> </div>
<div class="configur_content" v-if="tempObj.type === 'miniprogram'"> <div class="applet">
<div class="applet"> <span>小程序的备用网页</span>
<span>小程序的appid</span> <el-input class="input_width" v-model="tempObj.url" placeholder="不支持小程序的老版本客户端将打开本网页" clearable />
<el-input class="input_width" v-model="tempObj.appid" placeholder="请输入小程序的appid" clearable></el-input>
</div>
<div class="applet">
<span>小程序的页面路径</span>
<el-input class="input_width" v-model="tempObj.pagepath" placeholder="请输入小程序的页面路径pages/index" clearable></el-input>
</div>
<div class="applet">
<span>备用网页</span>
<el-input class="input_width" v-model="tempObj.url" placeholder="不支持小程序的老版本客户端将打开本网页" clearable></el-input>
</div>
<p class="blue">tips:需要和公众号进行关联才可以把小程序绑定带微信菜单上哟</p>
</div> </div>
<div class="configur_content" v-if="tempObj.type === 'article_view_limited'"> <p class="blue">tips:需要和公众号进行关联才可以把小程序绑定带微信菜单上哟</p>
<el-row> </div>
<div class="select-item" v-if="tempObj && tempObj.content && tempObj.content.articles"> <div class="configur_content" v-if="tempObj.type === 'article_view_limited'">
<WxNews :objData="tempObj.content.articles"></WxNews> <el-row>
<el-row class="ope-row"> <div class="select-item" v-if="tempObj && tempObj.content && tempObj.content.articles">
<el-button type="danger" icon="el-icon-delete" circle @click="deleteTempObj"></el-button> <WxNews :objData="tempObj.content.articles"></WxNews>
</el-row> <el-row class="ope-row">
</div> <el-button type="danger" icon="el-icon-delete" circle @click="deleteTempObj"></el-button>
<div v-if="!tempObj.content || !tempObj.content.articles"> </el-row>
<el-row> </div>
<el-col :span="24" style="text-align: center"> <div v-if="!tempObj.content || !tempObj.content.articles">
<el-button type="success" @click="openMaterial"><i class="el-icon-circle-check el-icon--right"></i></el-button> <el-row>
</el-col> <el-col :span="24" style="text-align: center">
</el-row> <el-button type="success" @click="openMaterial"><i class="el-icon-circle-check el-icon--right"></i></el-button>
</div> </el-col>
<el-dialog title="选择图文" :visible.sync="dialogNewsVisible" width="90%"> </el-row>
<WxMaterialSelect :objData="{repType:'news'}" @selectMaterial="selectMaterial"></WxMaterialSelect> </div>
</el-dialog> <el-dialog title="选择图文" :visible.sync="dialogNewsVisible" width="90%">
</el-row> <WxMaterialSelect :objData="{repType:'news'}" @selectMaterial="selectMaterial"></WxMaterialSelect>
</div> </el-dialog>
<div class="configur_content" v-if="tempObj.type === 'click' || tempObj.type === 'scancode_waitmsg'"> </el-row>
<WxReplySelect :objData="tempObj" v-if="hackResetWxReplySelect"></WxReplySelect> </div>
</div> <div class="configur_content" v-if="tempObj.type === 'click' || tempObj.type === 'scancode_waitmsg'">
</div> <wx-reply-select :objData="tempObj.reply" v-if="hackResetWxReplySelect" />
</div> </div>
</div>
</div>
</div> </div>
<!-- 一进页面就显示的默认页面当点击左边按钮的时候就不显示了--> <!-- 一进页面就显示的默认页面当点击左边按钮的时候就不显示了-->
<div v-else class="right"> <div v-else class="right">
@ -139,10 +146,10 @@ SOFTWARE.
</template> </template>
<script> <script>
import WxReplySelect from '@/views/mp/components/wx-news/main.vue' import WxReplySelect from '@/views/mp/components/wx-reply/main.vue'
import WxNews from '@/views/mp/components/wx-news/main.vue'; import WxNews from '@/views/mp/components/wx-news/main.vue';
import WxMaterialSelect from '@/views/mp/components/wx-news/main.vue' import WxMaterialSelect from '@/views/mp/components/wx-news/main.vue'
import {deleteMenu, getMenuList, saveMenu} from "@/api/mp/menu"; import { deleteMenu, getMenuList, saveMenu } from "@/api/mp/menu";
import { getSimpleAccounts } from "@/api/mp/account"; import { getSimpleAccounts } from "@/api/mp/account";
export default { export default {
@ -177,7 +184,7 @@ export default {
showConfigureContent: true, // showConfigureContent: true, //
hackResetWxReplySelect: false, // WxReplySelect hackResetWxReplySelect: false, // WxReplySelect
tempObj:{}, // tempObj: {}, //
tempSelfObj: { // tempObjmenu tempSelfObj: { // tempObjmenu
}, },
visible2: false, // "" visible2: false, // ""
@ -240,6 +247,7 @@ export default {
getList() { getList() {
this.loading = false; this.loading = false;
getMenuList(this.accountId).then(response => { getMenuList(this.accountId).then(response => {
response.data = this.convertMenuList(response.data);
this.menuList = this.handleTree(response.data, "id"); this.menuList = this.handleTree(response.data, "id");
}).finally(() => { }).finally(() => {
this.loading = false; this.loading = false;
@ -262,6 +270,39 @@ export default {
} }
this.handleQuery() this.handleQuery()
}, },
// menuList menuList
convertMenuList(list) {
const menuList = [];
list.forEach(item => {
const menu = {
...item,
};
if (item.type === 'click' || item.type === 'scancode_waitmsg') {
this.$delete(menu, 'replyMessageType');
this.$delete(menu, 'replyContent');
this.$delete(menu, 'replyMediaId');
this.$delete(menu, 'replyMediaUrl');
this.$delete(menu, 'replyDescription');
this.$delete(menu, 'replyArticles');
menu.reply = {
type: item.replyMessageType,
accountId: item.accountId,
content: item.replyContent,
mediaId: item.replyMediaId,
url: item.replyMediaUrl,
title: item.replyTitle,
description: item.replyDescription,
thumbMediaId: item.replyThumbMediaId,
thumbMediaUrl: item.replyThumbMediaUrl,
articles: item.replyArticles,
musicUrl: item.replyMusicUrl,
hqMusicUrl: item.replyHqMusicUrl,
}
}
menuList.push(menu);
});
return menuList;
},
// ======================== ======================== // ======================== ========================
// //
@ -285,7 +326,7 @@ export default {
// //
this.resetEditor(); this.resetEditor();
this.showRightFlag = true; // this.showRightFlag = true; //
this.tempObj = subItem;// this.tempObj = subItem; //
this.tempSelfObj.grand = "2"; // this.tempSelfObj.grand = "2"; //
this.tempSelfObj.index = index; // this.tempSelfObj.index = index; //
this.tempSelfObj.secondIndex = k; // this.tempSelfObj.secondIndex = k; //
@ -301,19 +342,27 @@ export default {
const menuKeyLength = this.menuList.length; const menuKeyLength = this.menuList.length;
const addButton = { const addButton = {
name: "菜单名称", name: "菜单名称",
children: [] children: [],
reply: { //
'type': 'text',
'accountId': this.accountId // 使
}
} }
this.$set(this.menuList, menuKeyLength, addButton) this.$set(this.menuList, menuKeyLength, addButton)
this.menuClick(this.menuKeyLength - 1, addButton) this.menuClick(this.menuKeyLength - 1, addButton)
}, },
// item // item
addSubMenu(i, item) { addSubMenu(i, item) {
if (!item.children || item.children.length <= 0){ // name
if (!item.children || item.children.length <= 0) {
this.$set( item, 'children',[]) this.$set( item, 'children',[])
// TODO
this.$delete( item, 'type') this.$delete( item, 'type')
this.$delete( item, 'pagepath') this.$delete( item, 'miniProgramAppId')
this.$delete( item, 'miniProgramPagePath')
this.$delete( item, 'url') this.$delete( item, 'url')
this.$delete( item, 'reply')
// TODO
this.$delete( item, 'key') this.$delete( item, 'key')
this.$delete( item, 'article_id') this.$delete( item, 'article_id')
this.$delete( item, 'textContent') this.$delete( item, 'textContent')
@ -322,7 +371,11 @@ export default {
let subMenuKeyLength = item.children.length; // key let subMenuKeyLength = item.children.length; // key
let addButton = { let addButton = {
name: "子菜单名称" name: "子菜单名称",
reply: { //
'type': 'text',
'accountId': this.accountId // 使
}
} }
this.$set(item.children, subMenuKeyLength, addButton); this.$set(item.children, subMenuKeyLength, addButton);
this.subMenuClick(item.children[subMenuKeyLength], i, subMenuKeyLength) this.subMenuClick(item.children[subMenuKeyLength], i, subMenuKeyLength)
@ -352,19 +405,19 @@ export default {
handleSave() { handleSave() {
this.$modal.confirm('确定要保证并发布该菜单吗?').then(() => { this.$modal.confirm('确定要保证并发布该菜单吗?').then(() => {
this.loading = true this.loading = true
return saveMenu(this.accountId, this.menuList); return saveMenu(this.accountId, this.convertMenuFormList());
}).then(() => { }).then(() => {
this.getList(); this.getList();
this.$modal.msgSuccess("发布成功"); this.$modal.msgSuccess("发布成功");
}).catch(() => {}).finally(() => { }).finally(() => {
this.loading = false this.loading = false
}); });
}, },
// Editor // Editor
resetEditor() { resetEditor() {
this.hackResetEditor = false // this.hackResetWxReplySelect = false //
this.$nextTick(() => { this.$nextTick(() => {
this.hackResetEditor = true // this.hackResetWxReplySelect = true //
}) })
}, },
handleDelete() { handleDelete() {
@ -378,6 +431,45 @@ export default {
this.loading = false this.loading = false
}); });
}, },
// menuList menuList
convertMenuFormList() {
const menuList = [];
this.menuList.forEach(item => {
let menu = this.convertMenuForm(item);
menuList.push(menu);
//
if (!item.children || item.children.length <= 0) {
return;
}
item.children = [];
item.children.forEach(subItem => {
menu.children.push(this.convertMenuForm(subItem))
})
})
return menuList;
},
// menu menu
convertMenuForm(menu) {
let result = {
...menu,
children: undefined, //
reply: undefined, //
}
if (menu.type === 'click' || menu.type === 'scancode_waitmsg') {
result.replyMessageType = menu.reply.type;
result.replyContent = menu.reply.content;
result.replyMediaId = menu.reply.mediaId;
result.replyMediaUrl = menu.reply.url;
result.replyTitle = menu.reply.title;
result.replyDescription = menu.reply.description;
result.replyThumbMediaId = menu.reply.thumbMediaId;
result.replyThumbMediaUrl = menu.reply.thumbMediaUrl;
result.replyArticles = menu.reply.articles;
result.replyMusicUrl = menu.reply.musicUrl;
result.replyHqMusicUrl = menu.reply.hqMusicUrl;
}
return result;
},
// TODO // TODO