mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2024-11-25 08:41:52 +08:00
mp:完善【菜单】的回复功能
This commit is contained in:
parent
141e4e4c8b
commit
0499226c3d
@ -1,12 +1,18 @@
|
||||
package cn.iocoder.yudao.module.mp.controller.admin.menu.vo;
|
||||
|
||||
import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
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;
|
||||
|
||||
// TODO 芋艿:完善 swagger 注解
|
||||
import static cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
|
||||
|
||||
/**
|
||||
* 微信菜单 Base VO,提供给添加、修改、详细的子 VO 使用
|
||||
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
|
||||
@ -38,70 +44,51 @@ public class MpMenuBaseVO {
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 网页链接
|
||||
*
|
||||
* 用户点击菜单可打开链接,不超过 1024 字节
|
||||
*
|
||||
* 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM
|
||||
*/
|
||||
@ApiModelProperty(value = "网页链接", example = "https://www.iocoder.cn/")
|
||||
@NotEmpty(message = "网页链接不能为空", groups = {ViewButtonGroup.class, MiniProgramButtonGroup.class})
|
||||
@URL(message = "网页链接必须是 URL 格式")
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 小程序的 appId
|
||||
*
|
||||
* 类型为 {@link WxConsts.MenuButtonType} 的 MINIPROGRAM
|
||||
*/
|
||||
@ApiModelProperty(value = "小程序的 appId", example = "wx1234567890")
|
||||
@NotEmpty(message = "小程序的 appId 不能为空", groups = MiniProgramButtonGroup.class)
|
||||
private String miniProgramAppId;
|
||||
/**
|
||||
* 小程序的页面路径
|
||||
*
|
||||
* 类型为 {@link WxConsts.MenuButtonType} 的 MINIPROGRAM
|
||||
*/
|
||||
|
||||
@ApiModelProperty(value = "小程序的页面路径", example = "pages/index/index")
|
||||
@NotEmpty(message = "小程序的页面路径不能为空", groups = MiniProgramButtonGroup.class)
|
||||
private String miniProgramPagePath;
|
||||
|
||||
// ========== 消息内容 ==========
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*
|
||||
* 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG
|
||||
*
|
||||
* 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS
|
||||
*/
|
||||
@ApiModelProperty(value = "消息类型", example = "text",
|
||||
notes = "枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC")
|
||||
@NotEmpty(message = "消息类型不能为空", groups = {ClickButtonGroup.class, ScanCodeWaitMsgButtonGroup.class})
|
||||
private String replyMessageType;
|
||||
|
||||
/**
|
||||
* 回复的消息内容
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT
|
||||
*/
|
||||
@ApiModelProperty(value = "回复的消息内容", example = "欢迎关注")
|
||||
@NotEmpty(message = "回复的消息内容不能为空", groups = {TextMessageGroup.class})
|
||||
private String replyContent;
|
||||
|
||||
/**
|
||||
* 回复的媒体 id
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
|
||||
*/
|
||||
@ApiModelProperty(value = "回复的媒体 id", example = "123456")
|
||||
@NotEmpty(message = "回复的消息 mediaId 不能为空",
|
||||
groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
|
||||
private String replyMediaId;
|
||||
/**
|
||||
* 回复的媒体 URL
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
|
||||
*/
|
||||
@ApiModelProperty(value = "回复的媒体 URL", example = "https://www.iocoder.cn/xxx.jpg")
|
||||
@NotEmpty(message = "回复的消息 mediaId 不能为空",
|
||||
groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
|
||||
private String replyMediaUrl;
|
||||
|
||||
/**
|
||||
* 回复的标题
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
|
||||
*/
|
||||
@ApiModelProperty(value = "缩略图的媒体 id", example = "123456")
|
||||
@NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class})
|
||||
private String replyThumbMediaId;
|
||||
@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;
|
||||
/**
|
||||
* 回复的描述
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
|
||||
*/
|
||||
@ApiModelProperty(value = "回复的描述", example = "视频描述")
|
||||
@NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
|
||||
private String replyDescription;
|
||||
|
||||
/**
|
||||
@ -109,6 +96,17 @@ public class MpMenuBaseVO {
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
|
||||
*/
|
||||
@NotNull(message = "回复的图文消息不能为空", groups = NewsMessageGroup.class)
|
||||
@Valid
|
||||
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;
|
||||
|
||||
}
|
||||
|
@ -26,28 +26,28 @@ public class MpMessageSendReqVO {
|
||||
public String type;
|
||||
|
||||
@ApiModelProperty(value = "消息内容", required = true, example = "你好呀")
|
||||
@NotEmpty(message = "消息内容不能为空", groups = TextGroup.class)
|
||||
@NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class)
|
||||
private String content;
|
||||
|
||||
@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;
|
||||
|
||||
@ApiModelProperty(value = "标题", required = true, example = "没有标题")
|
||||
@NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class)
|
||||
@NotEmpty(message = "消息内容不能为空", groups = VideoMessageGroup.class)
|
||||
private String title;
|
||||
|
||||
@ApiModelProperty(value = "描述", required = true, example = "你猜")
|
||||
@NotEmpty(message = "消息描述不能为空", groups = VideoGroup.class)
|
||||
@NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
|
||||
private String description;
|
||||
|
||||
@ApiModelProperty(value = "缩略图的媒体 id", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
|
||||
@NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicGroup.class)
|
||||
@NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicMessageGroup.class)
|
||||
private String thumbMediaId;
|
||||
|
||||
@ApiModelProperty(value = "图文消息", required = true)
|
||||
@Valid
|
||||
@NotNull(message = "图文消息不能为空", groups = NewsGroup.class)
|
||||
@NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class)
|
||||
private List<MpMessageDO.Article> articles;
|
||||
|
||||
@ApiModelProperty(value = "音乐链接", example = "https://www.iocoder.cn/music.mp3", notes = "消息类型为 MUSIC 时")
|
||||
|
@ -26,9 +26,12 @@ public interface MpMenuConvert {
|
||||
@Mapping(source = "menu.replyMessageType", target = "type"),
|
||||
@Mapping(source = "menu.replyContent", target = "content"),
|
||||
@Mapping(source = "menu.replyMediaId", target = "mediaId"),
|
||||
@Mapping(source = "menu.replyThumbMediaId", target = "thumbMediaId"),
|
||||
@Mapping(source = "menu.replyTitle", target = "title"),
|
||||
@Mapping(source = "menu.replyDescription", target = "description"),
|
||||
@Mapping(source = "menu.replyArticles", target = "articles"),
|
||||
@Mapping(source = "menu.replyMusicUrl", target = "musicUrl"),
|
||||
@Mapping(source = "menu.replyHqMusicUrl", target = "hqMusicUrl"),
|
||||
})
|
||||
MpMessageSendOutReqBO convert(String openid, MpMenuDO menu);
|
||||
|
||||
|
@ -17,7 +17,6 @@ public interface MpAutoReplyConvert {
|
||||
@Mapping(source = "reply.responseMessageType", target = "type"),
|
||||
@Mapping(source = "reply.responseContent", target = "content"),
|
||||
@Mapping(source = "reply.responseMediaId", target = "mediaId"),
|
||||
@Mapping(source = "reply.responseMediaUrl", target = "mediaUrl"),
|
||||
@Mapping(source = "reply.responseTitle", target = "title"),
|
||||
@Mapping(source = "reply.responseDescription", target = "description"),
|
||||
@Mapping(source = "reply.responseArticles", target = "articles"),
|
||||
|
@ -57,10 +57,14 @@ public interface MpMessageConvert {
|
||||
break;
|
||||
case WxConsts.XmlMsgType.IMAGE: // 2. 图片
|
||||
case WxConsts.XmlMsgType.VOICE: // 3. 语音
|
||||
message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl());
|
||||
message.setMediaId(sendReqBO.getMediaId())
|
||||
// .setMediaUrl(sendReqBO.getMediaUrl()) TODO 芋艿:去 url
|
||||
;
|
||||
break;
|
||||
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());
|
||||
break;
|
||||
case WxConsts.XmlMsgType.NEWS: // 5. 图文
|
||||
@ -69,7 +73,7 @@ public interface MpMessageConvert {
|
||||
message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription())
|
||||
.setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl())
|
||||
.setThumbMediaId(sendReqBO.getThumbMediaId());
|
||||
// .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:url 待确定
|
||||
// .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:去 url
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
|
||||
|
@ -103,7 +103,7 @@ public class MpMenuDO extends BaseDO {
|
||||
*
|
||||
* 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG
|
||||
*
|
||||
* 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS
|
||||
* 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC
|
||||
*/
|
||||
private String replyMessageType;
|
||||
|
||||
@ -140,6 +140,19 @@ public class MpMenuDO extends BaseDO {
|
||||
*/
|
||||
private String replyDescription;
|
||||
|
||||
/**
|
||||
* 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
|
||||
*/
|
||||
private String replyThumbMediaId;
|
||||
/**
|
||||
* 缩略图的媒体 URL
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
|
||||
*/
|
||||
private String replyThumbMediaUrl;
|
||||
|
||||
/**
|
||||
* 回复的图文消息数组
|
||||
*
|
||||
@ -148,4 +161,19 @@ public class MpMenuDO extends BaseDO {
|
||||
@TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class)
|
||||
private List<MpMessageDO.Article> replyArticles;
|
||||
|
||||
/**
|
||||
* 回复的音乐链接
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
|
||||
*/
|
||||
private String replyMusicUrl;
|
||||
/**
|
||||
* 回复的高质量音乐链接
|
||||
*
|
||||
* WIFI 环境优先使用该链接播放音乐
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
|
||||
*/
|
||||
private String replyHqMusicUrl;
|
||||
|
||||
}
|
||||
|
@ -124,13 +124,13 @@ public class MpMessageDO extends BaseDO {
|
||||
/**
|
||||
* 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
|
||||
*/
|
||||
private String thumbMediaId;
|
||||
/**
|
||||
* 缩略图的媒体 URL
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO
|
||||
*/
|
||||
private String thumbMediaUrl;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.api.WxConsts;
|
||||
@ -14,36 +15,6 @@ import javax.validation.Validator;
|
||||
@Slf4j
|
||||
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;
|
||||
switch (type) {
|
||||
case WxConsts.XmlMsgType.TEXT:
|
||||
group = TextGroup.class;
|
||||
group = TextMessageGroup.class;
|
||||
break;
|
||||
case WxConsts.XmlMsgType.IMAGE:
|
||||
group = ImageGroup.class;
|
||||
group = ImageMessageGroup.class;
|
||||
break;
|
||||
case WxConsts.XmlMsgType.VOICE:
|
||||
group = VoiceGroup.class;
|
||||
group = VoiceMessageGroup.class;
|
||||
break;
|
||||
case WxConsts.XmlMsgType.VIDEO:
|
||||
group = VideoGroup.class;
|
||||
group = VideoMessageGroup.class;
|
||||
break;
|
||||
case WxConsts.XmlMsgType.NEWS:
|
||||
group = NewsGroup.class;
|
||||
group = NewsMessageGroup.class;
|
||||
break;
|
||||
case WxConsts.XmlMsgType.MUSIC:
|
||||
group = MusicGroup.class;
|
||||
group = MusicMessageGroup.class;
|
||||
break;
|
||||
default:
|
||||
log.error("[validateMessage][未知的消息类型({})]", message);
|
||||
@ -80,6 +51,35 @@ public class MpUtils {
|
||||
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 {}
|
||||
}
|
||||
|
@ -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.mysql.menu.MpMenuMapper;
|
||||
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.message.MpMessageService;
|
||||
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 javax.annotation.Resource;
|
||||
import javax.validation.Validator;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
@ -48,6 +50,9 @@ public class MpMenuServiceImpl implements MpMenuService {
|
||||
@Lazy // 延迟加载,避免循环引用报错
|
||||
private MpServiceFactory mpServiceFactory;
|
||||
|
||||
@Resource
|
||||
private Validator validator;
|
||||
|
||||
@Resource
|
||||
private MpMenuMapper mpMenuMapper;
|
||||
|
||||
@ -57,6 +62,9 @@ public class MpMenuServiceImpl implements MpMenuService {
|
||||
MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId());
|
||||
WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId());
|
||||
|
||||
// 参数校验
|
||||
createReqVO.getMenus().forEach(this::validateMenu);
|
||||
|
||||
// 第一步,同步公众号
|
||||
WxMenu wxMenu = new WxMenu();
|
||||
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
|
||||
public void deleteMenuByAccountId(Long accountId) {
|
||||
WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
|
||||
@ -93,25 +144,6 @@ public class MpMenuServiceImpl implements MpMenuService {
|
||||
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
|
||||
public WxMpXmlOutMessage reply(String appId, String key, String openid) {
|
||||
// 第一步,获得菜单
|
||||
|
@ -76,8 +76,8 @@ public class MpMessageServiceImpl implements MpMessageService {
|
||||
Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser());
|
||||
|
||||
// 记录消息
|
||||
MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);
|
||||
message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
|
||||
MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user)
|
||||
.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
|
||||
downloadMessageMedia(message);
|
||||
mpMessageMapper.insert(message);
|
||||
}
|
||||
@ -94,9 +94,9 @@ public class MpMessageServiceImpl implements MpMessageService {
|
||||
Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid());
|
||||
|
||||
// 记录消息
|
||||
MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user);
|
||||
message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
|
||||
// TODO 芋艿:downloadMessageMedia
|
||||
MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user).
|
||||
setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
|
||||
downloadMessageMedia(message);
|
||||
mpMessageMapper.insert(message);
|
||||
|
||||
// 转换返回 WxMpXmlOutMessage 对象
|
||||
@ -122,8 +122,8 @@ public class MpMessageServiceImpl implements MpMessageService {
|
||||
}
|
||||
|
||||
// 记录消息
|
||||
MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);
|
||||
message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
|
||||
MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user)
|
||||
.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
|
||||
downloadMessageMedia(message);
|
||||
mpMessageMapper.insert(message);
|
||||
return message;
|
||||
|
@ -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 lombok.Data;
|
||||
import me.chanjar.weixin.common.api.WxConsts;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
@ -45,7 +46,7 @@ public class MpMessageSendOutReqBO {
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT
|
||||
*/
|
||||
@NotEmpty(message = "消息内容不能为空", groups = TextGroup.class)
|
||||
@NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class)
|
||||
private String content;
|
||||
|
||||
/**
|
||||
@ -53,39 +54,38 @@ public class MpMessageSendOutReqBO {
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
|
||||
*/
|
||||
@NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
|
||||
@NotEmpty(message = "消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
|
||||
private String mediaId;
|
||||
// TODO 芋艿:考虑去掉
|
||||
/**
|
||||
* 媒体 URL
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
|
||||
*/
|
||||
@NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
|
||||
private String mediaUrl;
|
||||
// // TODO 芋艿:考虑去掉
|
||||
// /**
|
||||
// * 媒体 URL
|
||||
// *
|
||||
// * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
|
||||
// */
|
||||
// @NotEmpty(message = "消息内容不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class})
|
||||
// private String mediaUrl;
|
||||
|
||||
/**
|
||||
* 缩略图的媒体 id
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC
|
||||
*/
|
||||
@NotEmpty(message = "消息内容不能为空", groups = {MusicGroup.class})
|
||||
@NotEmpty(message = "消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class})
|
||||
private String thumbMediaId;
|
||||
// TODO 芋艿:考虑去掉
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
|
||||
*/
|
||||
@NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class)
|
||||
@NotEmpty(message = "消息标题不能为空", groups = VideoMessageGroup.class)
|
||||
private String title;
|
||||
/**
|
||||
* 描述
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
|
||||
*/
|
||||
@NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class)
|
||||
@NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class)
|
||||
private String description;
|
||||
|
||||
/**
|
||||
@ -94,7 +94,7 @@ public class MpMessageSendOutReqBO {
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "图文消息不能为空", groups = NewsGroup.class)
|
||||
@NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class)
|
||||
private List<MpMessageDO.Article> articles;
|
||||
|
||||
/**
|
||||
@ -102,6 +102,8 @@ public class MpMessageSendOutReqBO {
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
|
||||
*/
|
||||
@NotEmpty(message = "音乐链接不能为空", groups = MusicMessageGroup.class)
|
||||
@URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
|
||||
private String musicUrl;
|
||||
|
||||
/**
|
||||
@ -109,6 +111,8 @@ public class MpMessageSendOutReqBO {
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
|
||||
*/
|
||||
@NotEmpty(message = "高质量音乐链接不能为空", groups = MusicMessageGroup.class)
|
||||
@URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class)
|
||||
private String hqMusicUrl;
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
芋道源码:
|
||||
① less 切到 scss,减少对 less 和 less-loader 的依赖
|
||||
②
|
||||
-->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
@ -75,10 +78,13 @@ SOFTWARE.
|
||||
</div>
|
||||
<div>
|
||||
<span>菜单名称:</span>
|
||||
<el-input class="input_width" v-model="tempObj.name" placeholder="请输入菜单名称" :maxlength="nameMaxLength"
|
||||
clearable />
|
||||
<el-input class="input_width" v-model="tempObj.name" placeholder="请输入菜单名称" :maxlength="nameMaxLength" clearable />
|
||||
</div>
|
||||
<div v-if="showConfigureContent">
|
||||
<div class="menu_content">
|
||||
<span>菜单标识:</span>
|
||||
<el-input class="input_width" v-model="tempObj.menuKey" placeholder="请输入菜单 KEY" clearable />
|
||||
</div>
|
||||
<div class="menu_content">
|
||||
<span>菜单内容:</span>
|
||||
<el-select v-model="tempObj.type" clearable placeholder="请选择" class="menu_option">
|
||||
@ -91,16 +97,17 @@ SOFTWARE.
|
||||
</div>
|
||||
<div class="configur_content" v-if="tempObj.type === 'miniprogram'">
|
||||
<div class="applet">
|
||||
<span>小程序的appid:</span>
|
||||
<el-input class="input_width" v-model="tempObj.appid" placeholder="请输入小程序的appid" clearable></el-input>
|
||||
<span>小程序的 appid :</span>
|
||||
<el-input class="input_width" v-model="tempObj.miniProgramAppId" placeholder="请输入小程序的appid" clearable />
|
||||
</div>
|
||||
<div class="applet">
|
||||
<span>小程序的页面路径:</span>
|
||||
<el-input class="input_width" v-model="tempObj.pagepath" placeholder="请输入小程序的页面路径,如:pages/index" clearable></el-input>
|
||||
<el-input class="input_width" v-model="tempObj.miniProgramPagePath"
|
||||
placeholder="请输入小程序的页面路径,如:pages/index" clearable />
|
||||
</div>
|
||||
<div class="applet">
|
||||
<span>备用网页:</span>
|
||||
<el-input class="input_width" v-model="tempObj.url" placeholder="不支持小程序的老版本客户端将打开本网页" clearable></el-input>
|
||||
<span>小程序的备用网页:</span>
|
||||
<el-input class="input_width" v-model="tempObj.url" placeholder="不支持小程序的老版本客户端将打开本网页" clearable />
|
||||
</div>
|
||||
<p class="blue">tips:需要和公众号进行关联才可以把小程序绑定带微信菜单上哟!</p>
|
||||
</div>
|
||||
@ -125,7 +132,7 @@ SOFTWARE.
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="configur_content" v-if="tempObj.type === 'click' || tempObj.type === 'scancode_waitmsg'">
|
||||
<WxReplySelect :objData="tempObj" v-if="hackResetWxReplySelect"></WxReplySelect>
|
||||
<wx-reply-select :objData="tempObj.reply" v-if="hackResetWxReplySelect" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -139,10 +146,10 @@ SOFTWARE.
|
||||
</template>
|
||||
|
||||
<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 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";
|
||||
|
||||
export default {
|
||||
@ -177,7 +184,7 @@ export default {
|
||||
showConfigureContent: true, // 是否展示配置内容;如果有子菜单,就不显示配置内容
|
||||
hackResetWxReplySelect: false, // 重置 WxReplySelect 组件
|
||||
|
||||
tempObj:{}, // 右边临时变量,作为中间值牵引关系
|
||||
tempObj: {}, // 右边临时变量,作为中间值牵引关系
|
||||
tempSelfObj: { // 一些临时值放在这里进行判断,如果放在 tempObj,由于引用关系,menu 也会多了多余的参数
|
||||
},
|
||||
visible2: false, //素材内容 "选择素材"按钮弹框显示隐藏
|
||||
@ -240,6 +247,7 @@ export default {
|
||||
getList() {
|
||||
this.loading = false;
|
||||
getMenuList(this.accountId).then(response => {
|
||||
response.data = this.convertMenuList(response.data);
|
||||
this.menuList = this.handleTree(response.data, "id");
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
@ -262,6 +270,39 @@ export default {
|
||||
}
|
||||
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.showRightFlag = true; // 右边菜单
|
||||
this.tempObj = subItem;//将点击的数据放到临时变量,对象有引用作用
|
||||
this.tempObj = subItem; // 将点击的数据放到临时变量,对象有引用作用
|
||||
this.tempSelfObj.grand = "2"; // 表示二级菜单
|
||||
this.tempSelfObj.index = index; // 表示一级菜单索引
|
||||
this.tempSelfObj.secondIndex = k; // 表示二级菜单索引
|
||||
@ -301,19 +342,27 @@ export default {
|
||||
const menuKeyLength = this.menuList.length;
|
||||
const addButton = {
|
||||
name: "菜单名称",
|
||||
children: []
|
||||
children: [],
|
||||
reply: { // 用于存储回复内容
|
||||
'type': 'text',
|
||||
'accountId': this.accountId // 保证组件里,可以使用到对应的公众号
|
||||
}
|
||||
}
|
||||
this.$set(this.menuList, menuKeyLength, addButton)
|
||||
this.menuClick(this.menuKeyLength - 1, addButton)
|
||||
},
|
||||
// 添加横向二级菜单;item 表示要操作的父菜单
|
||||
addSubMenu(i, item) {
|
||||
if (!item.children || item.children.length <= 0){
|
||||
// 清空父菜单的属性,因为它只需要 name 属性即可
|
||||
if (!item.children || item.children.length <= 0) {
|
||||
this.$set( item, 'children',[])
|
||||
// TODO 芋艿:需要搞的属性弄下
|
||||
this.$delete( item, 'type')
|
||||
this.$delete( item, 'pagepath')
|
||||
this.$delete( item, 'miniProgramAppId')
|
||||
this.$delete( item, 'miniProgramPagePath')
|
||||
this.$delete( item, 'url')
|
||||
this.$delete( item, 'reply')
|
||||
// TODO 芋艿:需要搞的属性弄下
|
||||
|
||||
this.$delete( item, 'key')
|
||||
this.$delete( item, 'article_id')
|
||||
this.$delete( item, 'textContent')
|
||||
@ -322,7 +371,11 @@ export default {
|
||||
|
||||
let subMenuKeyLength = item.children.length; // 获取二级菜单key长度
|
||||
let addButton = {
|
||||
name: "子菜单名称"
|
||||
name: "子菜单名称",
|
||||
reply: { // 用于存储回复内容
|
||||
'type': 'text',
|
||||
'accountId': this.accountId // 保证组件里,可以使用到对应的公众号
|
||||
}
|
||||
}
|
||||
this.$set(item.children, subMenuKeyLength, addButton);
|
||||
this.subMenuClick(item.children[subMenuKeyLength], i, subMenuKeyLength)
|
||||
@ -352,19 +405,19 @@ export default {
|
||||
handleSave() {
|
||||
this.$modal.confirm('确定要保证并发布该菜单吗?').then(() => {
|
||||
this.loading = true
|
||||
return saveMenu(this.accountId, this.menuList);
|
||||
return saveMenu(this.accountId, this.convertMenuFormList());
|
||||
}).then(() => {
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("发布成功");
|
||||
}).catch(() => {}).finally(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
});
|
||||
},
|
||||
// 表单 Editor 重置
|
||||
resetEditor() {
|
||||
this.hackResetEditor = false // 销毁组件
|
||||
this.hackResetWxReplySelect = false // 销毁组件
|
||||
this.$nextTick(() => {
|
||||
this.hackResetEditor = true // 重建组件
|
||||
this.hackResetWxReplySelect = true // 重建组件
|
||||
})
|
||||
},
|
||||
handleDelete() {
|
||||
@ -378,6 +431,45 @@ export default {
|
||||
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 芋艿:未归类
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user