Sfoglia il codice sorgente

mp:后端增加发送客服消息接口

YunaiV 2 anni fa
parent
commit
9cf88d1929

+ 28 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http

@@ -3,3 +3,31 @@ GET {{baseUrl}}/mp/message/page?accountId=1&pageNo=1&pageSize=10
 Content-Type: application/json
 Authorization: Bearer {{token}}
 tenant-id: {{adminTenentId}}
+
+### 请求 /mp/message/send 接口 => 成功(文本)
+POST {{baseUrl}}/mp/message/send
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "userId": 3,
+  "type": "text",
+  "content": "测试消息"
+}
+
+### 请求 /mp/message/send 接口 => 成功(音乐)
+POST {{baseUrl}}/mp/message/send
+Content-Type: application/json
+Authorization: Bearer {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+  "userId": 3,
+  "type": "music",
+  "title": "测试音乐标题",
+  "description": "测试音乐内容",
+  "musicUrl": "https://www.iocoder.cn/xx.mp3",
+  "hqMusicUrl": "https://www.iocoder.cn/xx_high.mp3",
+  "thumbMediaId": "s98Iveeg9vDVFwa9q0u8-zSfdKe3xIzAm7wCrFE4WKGPIo4d9qAhtC-n6qvnyWyH"
+}

+ 9 - 3
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO;
 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO;
 import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
 import cn.iocoder.yudao.module.mp.service.message.MpMessageService;
@@ -11,9 +12,7 @@ import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
@@ -37,4 +36,11 @@ public class MpMessageController {
         return success(MpMessageConvert.INSTANCE.convertPage(pageResult));
     }
 
+    @PostMapping("/send")
+//    @ApiOperation("获得粉丝消息表分页")
+//    @PreAuthorize("@ss.hasPermission('mp:message:query')")
+    public CommonResult<Long> sendMessage(@Valid @RequestBody MpMessageSendReqVO reqVO) {
+        return success(mpMessageService.sendKefuMessage(reqVO));
+    }
+
 }

+ 59 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/MpMessageSendReqVO.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.mp.controller.admin.message.vo;
+
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@ApiModel("管理后台 - 公众号消息发送 Request VO")
+@Data
+public class MpMessageSendReqVO {
+
+    @ApiModelProperty(value = "公众号粉丝的编号", required = true, example = "1024")
+    @NotNull(message = "公众号粉丝的编号不能为空")
+    private Long userId;
+
+    // ========== 消息内容 ==========
+
+    @ApiModelProperty(value = "消息类型", required = true, example = "text", notes = "TEXT/IMAGE/VOICE/VIDEO/NEWS")
+    @NotEmpty(message = "消息类型不能为空")
+    public String type;
+
+    @ApiModelProperty(value = "消息内容", required = true, example = "你好呀")
+    @NotEmpty(message = "消息内容不能为空", groups = TextGroup.class)
+    private String content;
+
+    @ApiModelProperty(value = "媒体 ID", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
+    @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
+    private String mediaId;
+
+    @ApiModelProperty(value = "标题", required = true, example = "没有标题")
+    @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class)
+    private String title;
+
+    @ApiModelProperty(value = "描述", required = true, example = "你猜")
+    @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class)
+    private String description;
+
+    @ApiModelProperty(value = "缩略图的媒体 id", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
+    @NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicGroup.class)
+    private String thumbMediaId;
+
+    @ApiModelProperty(value = "图文消息", required = true)
+    @Valid
+    @NotNull(message = "图文消息不能为空", groups = NewsGroup.class)
+    private List<MpMessageDO.Article> articles;
+
+    @ApiModelProperty(value = "音乐链接", example = "https://www.iocoder.cn/music.mp3", notes = "消息类型为 MUSIC 时")
+    private String musicUrl;
+
+    @ApiModelProperty(value = "高质量音乐链接", example = "https://www.iocoder.cn/music.mp3", notes = "消息类型为 MUSIC 时")
+    private String hqMusicUrl;
+
+}

+ 76 - 16
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java

@@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.mp.convert.message;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageRespVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
 import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
 import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage;
@@ -16,7 +18,6 @@ import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
-import java.util.Collections;
 import java.util.List;
 
 @Mapper
@@ -30,12 +31,6 @@ public interface MpMessageConvert {
 
     PageResult<MpMessageRespVO> convertPage(PageResult<MpMessageDO> page);
 
-    @Mappings(value = {
-            @Mapping(source = "msgType", target = "type"),
-            @Mapping(target = "createTime", ignore = true),
-    })
-    MpMessageDO convert(WxMpXmlMessage wxMessage);
-
     default MpMessageDO convert(WxMpXmlMessage wxMessage, MpAccountDO account, MpUserDO user) {
         MpMessageDO message = convert(wxMessage);
         if (account != null) {
@@ -46,6 +41,11 @@ public interface MpMessageConvert {
         }
         return message;
     }
+    @Mappings(value = {
+            @Mapping(source = "msgType", target = "type"),
+            @Mapping(target = "createTime", ignore = true),
+    })
+    MpMessageDO convert(WxMpXmlMessage bean);
 
     default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, MpAccountDO account, MpUserDO user) {
         // 构建消息
@@ -65,6 +65,11 @@ public interface MpMessageConvert {
                 break;
             case WxConsts.XmlMsgType.NEWS: // 5. 图文
                 message.setArticles(sendReqBO.getArticles());
+            case WxConsts.XmlMsgType.MUSIC: // 6. 音乐
+                message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription())
+                        .setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl())
+                        .setThumbMediaId(sendReqBO.getThumbMediaId());
+//                        .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:url 待确定
                 break;
             default:
                 throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
@@ -81,33 +86,88 @@ public interface MpMessageConvert {
     }
 
     default WxMpXmlOutMessage convert02(MpMessageDO message, MpAccountDO account) {
-        BaseBuilder<?, ? extends WxMpXmlOutMessage> messageBuilder;
+        BaseBuilder<?, ? extends WxMpXmlOutMessage> builder;
         // 个性化字段
         switch (message.getType()) {
             case WxConsts.XmlMsgType.TEXT:
-                messageBuilder = WxMpXmlOutMessage.TEXT().content(message.getContent());
+                builder = WxMpXmlOutMessage.TEXT().content(message.getContent());
                 break;
             case WxConsts.XmlMsgType.IMAGE:
-                messageBuilder = WxMpXmlOutMessage.IMAGE().mediaId(message.getMediaId());
+                builder = WxMpXmlOutMessage.IMAGE().mediaId(message.getMediaId());
                 break;
             case WxConsts.XmlMsgType.VOICE:
-                messageBuilder = WxMpXmlOutMessage.VOICE().mediaId(message.getMediaId());
+                builder = WxMpXmlOutMessage.VOICE().mediaId(message.getMediaId());
                 break;
             case WxConsts.XmlMsgType.VIDEO:
-                messageBuilder = WxMpXmlOutMessage.VIDEO().mediaId(message.getMediaId())
+                builder = WxMpXmlOutMessage.VIDEO().mediaId(message.getMediaId())
                         .title(message.getTitle()).description(message.getDescription());
                 break;
             case WxConsts.XmlMsgType.NEWS:
-                messageBuilder = WxMpXmlOutMessage.NEWS().articles(convertList02(message.getArticles()));
+                builder = WxMpXmlOutMessage.NEWS().articles(convertList02(message.getArticles()));
+                break;
+            case WxConsts.XmlMsgType.MUSIC:
+                builder = WxMpXmlOutMessage.MUSIC().title(message.getTitle()).description(message.getDescription())
+                        .musicUrl(message.getMusicUrl()).hqMusicUrl(message.getHqMusicUrl())
+                        .thumbMediaId(message.getThumbMediaId());
                 break;
             default:
                 throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
         }
         // 通用字段
-        messageBuilder.fromUser(account.getAccount());
-        messageBuilder.toUser(message.getOpenid());
-        return messageBuilder.build();
+        builder.fromUser(account.getAccount());
+        builder.toUser(message.getOpenid());
+        return builder.build();
     }
     List<WxMpXmlOutNewsMessage.Item> convertList02(List<MpMessageDO.Article> list);
 
+    default WxMpKefuMessage convert(MpMessageSendReqVO sendReqVO, MpUserDO user) {
+        me.chanjar.weixin.mp.builder.kefu.BaseBuilder<?> builder;
+        // 个性化字段
+        switch (sendReqVO.getType()) {
+            case WxConsts.KefuMsgType.TEXT:
+                builder = WxMpKefuMessage.TEXT().content(sendReqVO.getContent());
+                break;
+            case WxConsts.KefuMsgType.IMAGE:
+                builder = WxMpKefuMessage.IMAGE().mediaId(sendReqVO.getMediaId());
+                break;
+            case WxConsts.KefuMsgType.VOICE:
+                builder = WxMpKefuMessage.VOICE().mediaId(sendReqVO.getMediaId());
+                break;
+            case WxConsts.KefuMsgType.VIDEO:
+                builder = WxMpKefuMessage.VIDEO().mediaId(sendReqVO.getMediaId())
+                        .title(sendReqVO.getTitle()).description(sendReqVO.getDescription());
+                break;
+            case WxConsts.KefuMsgType.NEWS:
+                builder = WxMpKefuMessage.NEWS().articles(convertList03(sendReqVO.getArticles()));
+                break;
+            case WxConsts.KefuMsgType.MUSIC:
+                builder = WxMpKefuMessage.MUSIC().title(sendReqVO.getTitle()).description(sendReqVO.getDescription())
+                        .thumbMediaId(sendReqVO.getThumbMediaId())
+                        .musicUrl(sendReqVO.getMusicUrl()).hqMusicUrl(sendReqVO.getHqMusicUrl());
+                break;
+            default:
+                throw new IllegalArgumentException("不支持的消息类型:" + sendReqVO.getType());
+        }
+        // 通用字段
+        builder.toUser(user.getOpenid());
+        return builder.build();
+    }
+    List<WxMpKefuMessage.WxArticle> convertList03(List<MpMessageDO.Article> list);
+
+    default MpMessageDO convert(WxMpKefuMessage wxMessage, MpAccountDO account, MpUserDO user) {
+        MpMessageDO message = convert(wxMessage);
+        if (account != null) {
+            message.setAccountId(account.getId()).setAppId(account.getAppId());
+        }
+        if (user != null) {
+            message.setUserId(user.getId()).setOpenid(user.getOpenid());
+        }
+        return message;
+    }
+    @Mappings(value = {
+            @Mapping(source = "msgType", target = "type"),
+            @Mapping(target = "createTime", ignore = true),
+    })
+    MpMessageDO convert(WxMpKefuMessage bean);
+
 }

+ 16 - 1
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java

@@ -124,7 +124,7 @@ public class MpMessageDO extends BaseDO {
     /**
      * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
      *
-     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
      */
     private String thumbMediaId;
     /**
@@ -176,6 +176,21 @@ public class MpMessageDO extends BaseDO {
     @TableField(typeHandler = ArticleTypeHandler.class)
     private List<Article> articles;
 
+    /**
+     * 音乐链接
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String musicUrl;
+    /**
+     * 高质量音乐链接
+     *
+     * WIFI 环境优先使用该链接播放音乐
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String hqMusicUrl;
+
     // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
 
     /**

+ 8 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java

@@ -39,6 +39,11 @@ public class MpUtils {
      */
     public interface NewsGroup {}
 
+    /**
+     * Music 类型的消息,参数校验 Group
+     */
+    public interface MusicGroup {}
+
     /**
      * 校验消息的格式是否符合要求
      *
@@ -64,6 +69,9 @@ public class MpUtils {
             case WxConsts.XmlMsgType.NEWS:
                 group = NewsGroup.class;
                 break;
+            case WxConsts.XmlMsgType.MUSIC:
+                group = MusicGroup.class;
+                break;
             default:
                 log.error("[validateMessage][未知的消息类型({})]", message);
                 throw new IllegalArgumentException("不支持的消息类型:" + type);

+ 11 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.mp.service.message;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
 import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
@@ -45,4 +46,14 @@ public interface MpMessageService {
      */
     WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO);
 
+    /**
+     * 使用公众号,给用户发送【客服】消息
+     *
+     * 注意,该方法会真实发送消息
+     *
+     * @param sendReqVO 消息内容
+     * @return 消息编号
+     */
+    Long sendKefuMessage(MpMessageSendReqVO sendReqVO);
+
 }

+ 46 - 7
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java

@@ -7,6 +7,7 @@ import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.infra.api.file.FileApi;
 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO;
 import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert;
 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
@@ -21,6 +22,7 @@ import cn.iocoder.yudao.module.mp.service.user.MpUserService;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 import org.springframework.context.annotation.Lazy;
@@ -77,12 +79,7 @@ public class MpMessageServiceImpl implements MpMessageService {
         // 记录消息
         MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);
         message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
-        if (StrUtil.isNotEmpty(message.getMediaId())) {
-            message.setMediaUrl(mediaDownload(mpService, message.getMediaId()));
-        }
-        if (StrUtil.isNotEmpty(message.getThumbMediaId())) {
-            message.setThumbMediaUrl(mediaDownload(mpService, message.getThumbMediaId()));
-        }
+        downloadMessageMedia(mpService, message);
         mpMessageMapper.insert(message);
     }
 
@@ -106,6 +103,48 @@ public class MpMessageServiceImpl implements MpMessageService {
         return MpMessageConvert.INSTANCE.convert02(message, account);
     }
 
+    @Override
+    public Long sendKefuMessage(MpMessageSendReqVO sendReqVO) {
+        // 校验消息格式
+        MpUtils.validateMessage(validator, sendReqVO.getType(), sendReqVO);
+
+        // 获得关联信息
+        MpUserDO user = mpUserService.getRequiredUser(sendReqVO.getUserId());
+        MpAccountDO account = mpAccountService.getRequiredAccount(user.getAccountId());
+
+        // 发送客服消息
+        WxMpKefuMessage wxMessage = MpMessageConvert.INSTANCE.convert(sendReqVO, user);
+        WxMpService mpService = mpServiceFactory.getRequiredMpService(user.getAppId());
+        try {
+            boolean result = mpService.getKefuService().sendKefuMessage(wxMessage);
+            System.out.println(result);
+        } catch (WxErrorException e) {
+            throw new RuntimeException(e);
+        }
+
+        // 记录消息
+        MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);
+        message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
+        downloadMessageMedia(mpService, message);
+        mpMessageMapper.insert(message);
+        return message.getId();
+    }
+
+    /**
+     * 下载消息使用到的媒体文件,并上传到文件服务
+     *
+     * @param mpService 公众号 Service
+     * @param message 消息
+     */
+    private void downloadMessageMedia(WxMpService mpService, MpMessageDO message) {
+        if (StrUtil.isNotEmpty(message.getMediaId())) {
+            message.setMediaUrl(downloadMedia(mpService, message.getMediaId()));
+        }
+        if (StrUtil.isNotEmpty(message.getThumbMediaId())) {
+            message.setThumbMediaUrl(downloadMedia(mpService, message.getThumbMediaId()));
+        }
+    }
+
     /**
      * 下载微信媒体文件的内容,并上传到文件服务
      *
@@ -115,7 +154,7 @@ public class MpMessageServiceImpl implements MpMessageService {
      * @param mediaId 媒体文件编号
      * @return 上传后的 URL
      */
-    private String mediaDownload(WxMpService mpService, String mediaId) {
+    private String downloadMedia(WxMpService mpService, String mediaId) {
         try {
             // 第一步,从公众号下载媒体文件
             File file = mpService.getMaterialService().mediaDownload(mediaId);

+ 25 - 2
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.module.mp.service.message.bo;
 
 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 me.chanjar.weixin.common.api.WxConsts;
@@ -36,7 +35,7 @@ public class MpMessageSendOutReqBO {
     /**
      * 消息类型
      *
-     * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS
+     * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC
      */
     @NotEmpty(message = "消息类型不能为空")
     public String type;
@@ -56,6 +55,7 @@ public class MpMessageSendOutReqBO {
      */
     @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
     private String mediaId;
+    // TODO 芋艿:考虑去掉
     /**
      * 媒体 URL
      *
@@ -64,6 +64,15 @@ public class MpMessageSendOutReqBO {
     @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
     private String mediaUrl;
 
+    /**
+     * 缩略图的媒体 id
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC
+     */
+    @NotEmpty(message = "消息内容不能为空", groups = {MusicGroup.class})
+    private String thumbMediaId;
+    // TODO 芋艿:考虑去掉
+
     /**
      * 标题
      *
@@ -88,4 +97,18 @@ public class MpMessageSendOutReqBO {
     @NotNull(message = "图文消息不能为空", groups = NewsGroup.class)
     private List<MpMessageDO.Article> articles;
 
+    /**
+     * 音乐链接
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String musicUrl;
+
+    /**
+     * 高质量音乐链接
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     */
+    private String hqMusicUrl;
+
 }

+ 17 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserService.java

@@ -9,6 +9,9 @@ import me.chanjar.weixin.mp.bean.result.WxMpUser;
 import java.util.Collection;
 import java.util.List;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.USER_NOT_EXISTS;
+
 /**
  * 公众号粉丝 Service 接口
  *
@@ -33,6 +36,20 @@ public interface MpUserService {
      */
     MpUserDO getUser(String appId, String openId);
 
+    /**
+     * 获得公众号粉丝
+     *
+     * @param id 编号
+     * @return 公众号粉丝
+     */
+    default MpUserDO getRequiredUser(Long id) {
+        MpUserDO user = getUser(id);
+        if (user == null) {
+            throw exception(USER_NOT_EXISTS);
+        }
+        return user;
+    }
+
     /**
      * 获得公众号粉丝列表
      *

+ 0 - 1
yudao-ui-admin/src/views/mp/components/wx-msg/main.vue

@@ -140,7 +140,6 @@
           nickname: '公众号',
           avatar: require("@/assets/images/wechat.png"),
         },
-        qqMapKey: '' //
       }
     },
     created() {