Commit 9cf88d1929abe9fc24124d41fdc3119d30e30da8

Authored by YunaiV
1 parent f8eed5dd

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

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 @@ -3,3 +3,31 @@ GET {{baseUrl}}/mp/message/page?accountId=1&pageNo=1&pageSize=10
3 Content-Type: application/json 3 Content-Type: application/json
4 Authorization: Bearer {{token}} 4 Authorization: Bearer {{token}}
5 tenant-id: {{adminTenentId}} 5 tenant-id: {{adminTenentId}}
  6 +
  7 +### 请求 /mp/message/send 接口 => 成功(文本)
  8 +POST {{baseUrl}}/mp/message/send
  9 +Content-Type: application/json
  10 +Authorization: Bearer {{token}}
  11 +tenant-id: {{adminTenentId}}
  12 +
  13 +{
  14 + "userId": 3,
  15 + "type": "text",
  16 + "content": "测试消息"
  17 +}
  18 +
  19 +### 请求 /mp/message/send 接口 => 成功(音乐)
  20 +POST {{baseUrl}}/mp/message/send
  21 +Content-Type: application/json
  22 +Authorization: Bearer {{token}}
  23 +tenant-id: {{adminTenentId}}
  24 +
  25 +{
  26 + "userId": 3,
  27 + "type": "music",
  28 + "title": "测试音乐标题",
  29 + "description": "测试音乐内容",
  30 + "musicUrl": "https://www.iocoder.cn/xx.mp3",
  31 + "hqMusicUrl": "https://www.iocoder.cn/xx_high.mp3",
  32 + "thumbMediaId": "s98Iveeg9vDVFwa9q0u8-zSfdKe3xIzAm7wCrFE4WKGPIo4d9qAhtC-n6qvnyWyH"
  33 +}
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; @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; 5 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO;
6 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageRespVO; 6 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageRespVO;
  7 +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO;
7 import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert; 8 import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert;
8 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; 9 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
9 import cn.iocoder.yudao.module.mp.service.message.MpMessageService; 10 import cn.iocoder.yudao.module.mp.service.message.MpMessageService;
@@ -11,9 +12,7 @@ import io.swagger.annotations.Api; @@ -11,9 +12,7 @@ import io.swagger.annotations.Api;
11 import io.swagger.annotations.ApiOperation; 12 import io.swagger.annotations.ApiOperation;
12 import org.springframework.security.access.prepost.PreAuthorize; 13 import org.springframework.security.access.prepost.PreAuthorize;
13 import org.springframework.validation.annotation.Validated; 14 import org.springframework.validation.annotation.Validated;
14 -import org.springframework.web.bind.annotation.GetMapping;  
15 -import org.springframework.web.bind.annotation.RequestMapping;  
16 -import org.springframework.web.bind.annotation.RestController; 15 +import org.springframework.web.bind.annotation.*;
17 16
18 import javax.annotation.Resource; 17 import javax.annotation.Resource;
19 import javax.validation.Valid; 18 import javax.validation.Valid;
@@ -37,4 +36,11 @@ public class MpMessageController { @@ -37,4 +36,11 @@ public class MpMessageController {
37 return success(MpMessageConvert.INSTANCE.convertPage(pageResult)); 36 return success(MpMessageConvert.INSTANCE.convertPage(pageResult));
38 } 37 }
39 38
  39 + @PostMapping("/send")
  40 +// @ApiOperation("获得粉丝消息表分页")
  41 +// @PreAuthorize("@ss.hasPermission('mp:message:query')")
  42 + public CommonResult<Long> sendMessage(@Valid @RequestBody MpMessageSendReqVO reqVO) {
  43 + return success(mpMessageService.sendKefuMessage(reqVO));
  44 + }
  45 +
40 } 46 }
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/MpMessageSendReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mp.controller.admin.message.vo;
  2 +
  3 +import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
  4 +import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
  5 +import io.swagger.annotations.ApiModel;
  6 +import io.swagger.annotations.ApiModelProperty;
  7 +import lombok.Data;
  8 +
  9 +import javax.validation.Valid;
  10 +import javax.validation.constraints.NotEmpty;
  11 +import javax.validation.constraints.NotNull;
  12 +import java.util.List;
  13 +
  14 +@ApiModel("管理后台 - 公众号消息发送 Request VO")
  15 +@Data
  16 +public class MpMessageSendReqVO {
  17 +
  18 + @ApiModelProperty(value = "公众号粉丝的编号", required = true, example = "1024")
  19 + @NotNull(message = "公众号粉丝的编号不能为空")
  20 + private Long userId;
  21 +
  22 + // ========== 消息内容 ==========
  23 +
  24 + @ApiModelProperty(value = "消息类型", required = true, example = "text", notes = "TEXT/IMAGE/VOICE/VIDEO/NEWS")
  25 + @NotEmpty(message = "消息类型不能为空")
  26 + public String type;
  27 +
  28 + @ApiModelProperty(value = "消息内容", required = true, example = "你好呀")
  29 + @NotEmpty(message = "消息内容不能为空", groups = TextGroup.class)
  30 + private String content;
  31 +
  32 + @ApiModelProperty(value = "媒体 ID", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
  33 + @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
  34 + private String mediaId;
  35 +
  36 + @ApiModelProperty(value = "标题", required = true, example = "没有标题")
  37 + @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class)
  38 + private String title;
  39 +
  40 + @ApiModelProperty(value = "描述", required = true, example = "你猜")
  41 + @NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class)
  42 + private String description;
  43 +
  44 + @ApiModelProperty(value = "缩略图的媒体 id", required = true, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP")
  45 + @NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicGroup.class)
  46 + private String thumbMediaId;
  47 +
  48 + @ApiModelProperty(value = "图文消息", required = true)
  49 + @Valid
  50 + @NotNull(message = "图文消息不能为空", groups = NewsGroup.class)
  51 + private List<MpMessageDO.Article> articles;
  52 +
  53 + @ApiModelProperty(value = "音乐链接", example = "https://www.iocoder.cn/music.mp3", notes = "消息类型为 MUSIC 时")
  54 + private String musicUrl;
  55 +
  56 + @ApiModelProperty(value = "高质量音乐链接", example = "https://www.iocoder.cn/music.mp3", notes = "消息类型为 MUSIC 时")
  57 + private String hqMusicUrl;
  58 +
  59 +}
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; @@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.mp.convert.message;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageRespVO; 4 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageRespVO;
  5 +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO;
5 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; 6 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
6 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; 7 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
7 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; 8 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
8 import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; 9 import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
9 import me.chanjar.weixin.common.api.WxConsts; 10 import me.chanjar.weixin.common.api.WxConsts;
  11 +import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
10 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; 12 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
11 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; 13 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
12 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage; 14 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage;
@@ -16,7 +18,6 @@ import org.mapstruct.Mapping; @@ -16,7 +18,6 @@ import org.mapstruct.Mapping;
16 import org.mapstruct.Mappings; 18 import org.mapstruct.Mappings;
17 import org.mapstruct.factory.Mappers; 19 import org.mapstruct.factory.Mappers;
18 20
19 -import java.util.Collections;  
20 import java.util.List; 21 import java.util.List;
21 22
22 @Mapper 23 @Mapper
@@ -30,12 +31,6 @@ public interface MpMessageConvert { @@ -30,12 +31,6 @@ public interface MpMessageConvert {
30 31
31 PageResult<MpMessageRespVO> convertPage(PageResult<MpMessageDO> page); 32 PageResult<MpMessageRespVO> convertPage(PageResult<MpMessageDO> page);
32 33
33 - @Mappings(value = {  
34 - @Mapping(source = "msgType", target = "type"),  
35 - @Mapping(target = "createTime", ignore = true),  
36 - })  
37 - MpMessageDO convert(WxMpXmlMessage wxMessage);  
38 -  
39 default MpMessageDO convert(WxMpXmlMessage wxMessage, MpAccountDO account, MpUserDO user) { 34 default MpMessageDO convert(WxMpXmlMessage wxMessage, MpAccountDO account, MpUserDO user) {
40 MpMessageDO message = convert(wxMessage); 35 MpMessageDO message = convert(wxMessage);
41 if (account != null) { 36 if (account != null) {
@@ -46,6 +41,11 @@ public interface MpMessageConvert { @@ -46,6 +41,11 @@ public interface MpMessageConvert {
46 } 41 }
47 return message; 42 return message;
48 } 43 }
  44 + @Mappings(value = {
  45 + @Mapping(source = "msgType", target = "type"),
  46 + @Mapping(target = "createTime", ignore = true),
  47 + })
  48 + MpMessageDO convert(WxMpXmlMessage bean);
49 49
50 default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, MpAccountDO account, MpUserDO user) { 50 default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, MpAccountDO account, MpUserDO user) {
51 // 构建消息 51 // 构建消息
@@ -65,6 +65,11 @@ public interface MpMessageConvert { @@ -65,6 +65,11 @@ public interface MpMessageConvert {
65 break; 65 break;
66 case WxConsts.XmlMsgType.NEWS: // 5. 图文 66 case WxConsts.XmlMsgType.NEWS: // 5. 图文
67 message.setArticles(sendReqBO.getArticles()); 67 message.setArticles(sendReqBO.getArticles());
  68 + case WxConsts.XmlMsgType.MUSIC: // 6. 音乐
  69 + message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription())
  70 + .setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl())
  71 + .setThumbMediaId(sendReqBO.getThumbMediaId());
  72 +// .setThumbMediaUrl(sendReqBO.getThumbMediaUrl()); TODO 芋艿:url 待确定
68 break; 73 break;
69 default: 74 default:
70 throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); 75 throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
@@ -81,33 +86,88 @@ public interface MpMessageConvert { @@ -81,33 +86,88 @@ public interface MpMessageConvert {
81 } 86 }
82 87
83 default WxMpXmlOutMessage convert02(MpMessageDO message, MpAccountDO account) { 88 default WxMpXmlOutMessage convert02(MpMessageDO message, MpAccountDO account) {
84 - BaseBuilder<?, ? extends WxMpXmlOutMessage> messageBuilder; 89 + BaseBuilder<?, ? extends WxMpXmlOutMessage> builder;
85 // 个性化字段 90 // 个性化字段
86 switch (message.getType()) { 91 switch (message.getType()) {
87 case WxConsts.XmlMsgType.TEXT: 92 case WxConsts.XmlMsgType.TEXT:
88 - messageBuilder = WxMpXmlOutMessage.TEXT().content(message.getContent()); 93 + builder = WxMpXmlOutMessage.TEXT().content(message.getContent());
89 break; 94 break;
90 case WxConsts.XmlMsgType.IMAGE: 95 case WxConsts.XmlMsgType.IMAGE:
91 - messageBuilder = WxMpXmlOutMessage.IMAGE().mediaId(message.getMediaId()); 96 + builder = WxMpXmlOutMessage.IMAGE().mediaId(message.getMediaId());
92 break; 97 break;
93 case WxConsts.XmlMsgType.VOICE: 98 case WxConsts.XmlMsgType.VOICE:
94 - messageBuilder = WxMpXmlOutMessage.VOICE().mediaId(message.getMediaId()); 99 + builder = WxMpXmlOutMessage.VOICE().mediaId(message.getMediaId());
95 break; 100 break;
96 case WxConsts.XmlMsgType.VIDEO: 101 case WxConsts.XmlMsgType.VIDEO:
97 - messageBuilder = WxMpXmlOutMessage.VIDEO().mediaId(message.getMediaId()) 102 + builder = WxMpXmlOutMessage.VIDEO().mediaId(message.getMediaId())
98 .title(message.getTitle()).description(message.getDescription()); 103 .title(message.getTitle()).description(message.getDescription());
99 break; 104 break;
100 case WxConsts.XmlMsgType.NEWS: 105 case WxConsts.XmlMsgType.NEWS:
101 - messageBuilder = WxMpXmlOutMessage.NEWS().articles(convertList02(message.getArticles())); 106 + builder = WxMpXmlOutMessage.NEWS().articles(convertList02(message.getArticles()));
  107 + break;
  108 + case WxConsts.XmlMsgType.MUSIC:
  109 + builder = WxMpXmlOutMessage.MUSIC().title(message.getTitle()).description(message.getDescription())
  110 + .musicUrl(message.getMusicUrl()).hqMusicUrl(message.getHqMusicUrl())
  111 + .thumbMediaId(message.getThumbMediaId());
102 break; 112 break;
103 default: 113 default:
104 throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); 114 throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
105 } 115 }
106 // 通用字段 116 // 通用字段
107 - messageBuilder.fromUser(account.getAccount());  
108 - messageBuilder.toUser(message.getOpenid());  
109 - return messageBuilder.build(); 117 + builder.fromUser(account.getAccount());
  118 + builder.toUser(message.getOpenid());
  119 + return builder.build();
110 } 120 }
111 List<WxMpXmlOutNewsMessage.Item> convertList02(List<MpMessageDO.Article> list); 121 List<WxMpXmlOutNewsMessage.Item> convertList02(List<MpMessageDO.Article> list);
112 122
  123 + default WxMpKefuMessage convert(MpMessageSendReqVO sendReqVO, MpUserDO user) {
  124 + me.chanjar.weixin.mp.builder.kefu.BaseBuilder<?> builder;
  125 + // 个性化字段
  126 + switch (sendReqVO.getType()) {
  127 + case WxConsts.KefuMsgType.TEXT:
  128 + builder = WxMpKefuMessage.TEXT().content(sendReqVO.getContent());
  129 + break;
  130 + case WxConsts.KefuMsgType.IMAGE:
  131 + builder = WxMpKefuMessage.IMAGE().mediaId(sendReqVO.getMediaId());
  132 + break;
  133 + case WxConsts.KefuMsgType.VOICE:
  134 + builder = WxMpKefuMessage.VOICE().mediaId(sendReqVO.getMediaId());
  135 + break;
  136 + case WxConsts.KefuMsgType.VIDEO:
  137 + builder = WxMpKefuMessage.VIDEO().mediaId(sendReqVO.getMediaId())
  138 + .title(sendReqVO.getTitle()).description(sendReqVO.getDescription());
  139 + break;
  140 + case WxConsts.KefuMsgType.NEWS:
  141 + builder = WxMpKefuMessage.NEWS().articles(convertList03(sendReqVO.getArticles()));
  142 + break;
  143 + case WxConsts.KefuMsgType.MUSIC:
  144 + builder = WxMpKefuMessage.MUSIC().title(sendReqVO.getTitle()).description(sendReqVO.getDescription())
  145 + .thumbMediaId(sendReqVO.getThumbMediaId())
  146 + .musicUrl(sendReqVO.getMusicUrl()).hqMusicUrl(sendReqVO.getHqMusicUrl());
  147 + break;
  148 + default:
  149 + throw new IllegalArgumentException("不支持的消息类型:" + sendReqVO.getType());
  150 + }
  151 + // 通用字段
  152 + builder.toUser(user.getOpenid());
  153 + return builder.build();
  154 + }
  155 + List<WxMpKefuMessage.WxArticle> convertList03(List<MpMessageDO.Article> list);
  156 +
  157 + default MpMessageDO convert(WxMpKefuMessage wxMessage, MpAccountDO account, MpUserDO user) {
  158 + MpMessageDO message = convert(wxMessage);
  159 + if (account != null) {
  160 + message.setAccountId(account.getId()).setAppId(account.getAppId());
  161 + }
  162 + if (user != null) {
  163 + message.setUserId(user.getId()).setOpenid(user.getOpenid());
  164 + }
  165 + return message;
  166 + }
  167 + @Mappings(value = {
  168 + @Mapping(source = "msgType", target = "type"),
  169 + @Mapping(target = "createTime", ignore = true),
  170 + })
  171 + MpMessageDO convert(WxMpKefuMessage bean);
  172 +
113 } 173 }
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 { @@ -124,7 +124,7 @@ public class MpMessageDO extends BaseDO {
124 /** 124 /**
125 * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id 125 * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
126 * 126 *
127 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO 127 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
128 */ 128 */
129 private String thumbMediaId; 129 private String thumbMediaId;
130 /** 130 /**
@@ -176,6 +176,21 @@ public class MpMessageDO extends BaseDO { @@ -176,6 +176,21 @@ public class MpMessageDO extends BaseDO {
176 @TableField(typeHandler = ArticleTypeHandler.class) 176 @TableField(typeHandler = ArticleTypeHandler.class)
177 private List<Article> articles; 177 private List<Article> articles;
178 178
  179 + /**
  180 + * 音乐链接
  181 + *
  182 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
  183 + */
  184 + private String musicUrl;
  185 + /**
  186 + * 高质量音乐链接
  187 + *
  188 + * WIFI 环境优先使用该链接播放音乐
  189 + *
  190 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
  191 + */
  192 + private String hqMusicUrl;
  193 +
179 // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html 194 // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
180 195
181 /** 196 /**
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/util/MpUtils.java
@@ -40,6 +40,11 @@ public class MpUtils { @@ -40,6 +40,11 @@ public class MpUtils {
40 public interface NewsGroup {} 40 public interface NewsGroup {}
41 41
42 /** 42 /**
  43 + * Music 类型的消息,参数校验 Group
  44 + */
  45 + public interface MusicGroup {}
  46 +
  47 + /**
43 * 校验消息的格式是否符合要求 48 * 校验消息的格式是否符合要求
44 * 49 *
45 * @param type 类型 50 * @param type 类型
@@ -64,6 +69,9 @@ public class MpUtils { @@ -64,6 +69,9 @@ public class MpUtils {
64 case WxConsts.XmlMsgType.NEWS: 69 case WxConsts.XmlMsgType.NEWS:
65 group = NewsGroup.class; 70 group = NewsGroup.class;
66 break; 71 break;
  72 + case WxConsts.XmlMsgType.MUSIC:
  73 + group = MusicGroup.class;
  74 + break;
67 default: 75 default:
68 log.error("[validateMessage][未知的消息类型({})]", message); 76 log.error("[validateMessage][未知的消息类型({})]", message);
69 throw new IllegalArgumentException("不支持的消息类型:" + type); 77 throw new IllegalArgumentException("不支持的消息类型:" + type);
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; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.mp.service.message;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; 4 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO;
  5 +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO;
5 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; 6 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
6 import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO; 7 import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
7 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; 8 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
@@ -45,4 +46,14 @@ public interface MpMessageService { @@ -45,4 +46,14 @@ public interface MpMessageService {
45 */ 46 */
46 WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO); 47 WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO);
47 48
  49 + /**
  50 + * 使用公众号,给用户发送【客服】消息
  51 + *
  52 + * 注意,该方法会真实发送消息
  53 + *
  54 + * @param sendReqVO 消息内容
  55 + * @return 消息编号
  56 + */
  57 + Long sendKefuMessage(MpMessageSendReqVO sendReqVO);
  58 +
48 } 59 }
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; @@ -7,6 +7,7 @@ import cn.hutool.core.util.StrUtil;
7 import cn.iocoder.yudao.framework.common.pojo.PageResult; 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
8 import cn.iocoder.yudao.module.infra.api.file.FileApi; 8 import cn.iocoder.yudao.module.infra.api.file.FileApi;
9 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; 9 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO;
  10 +import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageSendReqVO;
10 import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert; 11 import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert;
11 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; 12 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
12 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; 13 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
@@ -21,6 +22,7 @@ import cn.iocoder.yudao.module.mp.service.user.MpUserService; @@ -21,6 +22,7 @@ import cn.iocoder.yudao.module.mp.service.user.MpUserService;
21 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
22 import me.chanjar.weixin.common.error.WxErrorException; 23 import me.chanjar.weixin.common.error.WxErrorException;
23 import me.chanjar.weixin.mp.api.WxMpService; 24 import me.chanjar.weixin.mp.api.WxMpService;
  25 +import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
24 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; 26 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
25 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; 27 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
26 import org.springframework.context.annotation.Lazy; 28 import org.springframework.context.annotation.Lazy;
@@ -77,12 +79,7 @@ public class MpMessageServiceImpl implements MpMessageService { @@ -77,12 +79,7 @@ public class MpMessageServiceImpl implements MpMessageService {
77 // 记录消息 79 // 记录消息
78 MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user); 80 MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);
79 message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom()); 81 message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
80 - if (StrUtil.isNotEmpty(message.getMediaId())) {  
81 - message.setMediaUrl(mediaDownload(mpService, message.getMediaId()));  
82 - }  
83 - if (StrUtil.isNotEmpty(message.getThumbMediaId())) {  
84 - message.setThumbMediaUrl(mediaDownload(mpService, message.getThumbMediaId()));  
85 - } 82 + downloadMessageMedia(mpService, message);
86 mpMessageMapper.insert(message); 83 mpMessageMapper.insert(message);
87 } 84 }
88 85
@@ -106,6 +103,48 @@ public class MpMessageServiceImpl implements MpMessageService { @@ -106,6 +103,48 @@ public class MpMessageServiceImpl implements MpMessageService {
106 return MpMessageConvert.INSTANCE.convert02(message, account); 103 return MpMessageConvert.INSTANCE.convert02(message, account);
107 } 104 }
108 105
  106 + @Override
  107 + public Long sendKefuMessage(MpMessageSendReqVO sendReqVO) {
  108 + // 校验消息格式
  109 + MpUtils.validateMessage(validator, sendReqVO.getType(), sendReqVO);
  110 +
  111 + // 获得关联信息
  112 + MpUserDO user = mpUserService.getRequiredUser(sendReqVO.getUserId());
  113 + MpAccountDO account = mpAccountService.getRequiredAccount(user.getAccountId());
  114 +
  115 + // 发送客服消息
  116 + WxMpKefuMessage wxMessage = MpMessageConvert.INSTANCE.convert(sendReqVO, user);
  117 + WxMpService mpService = mpServiceFactory.getRequiredMpService(user.getAppId());
  118 + try {
  119 + boolean result = mpService.getKefuService().sendKefuMessage(wxMessage);
  120 + System.out.println(result);
  121 + } catch (WxErrorException e) {
  122 + throw new RuntimeException(e);
  123 + }
  124 +
  125 + // 记录消息
  126 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);
  127 + message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
  128 + downloadMessageMedia(mpService, message);
  129 + mpMessageMapper.insert(message);
  130 + return message.getId();
  131 + }
  132 +
  133 + /**
  134 + * 下载消息使用到的媒体文件,并上传到文件服务
  135 + *
  136 + * @param mpService 公众号 Service
  137 + * @param message 消息
  138 + */
  139 + private void downloadMessageMedia(WxMpService mpService, MpMessageDO message) {
  140 + if (StrUtil.isNotEmpty(message.getMediaId())) {
  141 + message.setMediaUrl(downloadMedia(mpService, message.getMediaId()));
  142 + }
  143 + if (StrUtil.isNotEmpty(message.getThumbMediaId())) {
  144 + message.setThumbMediaUrl(downloadMedia(mpService, message.getThumbMediaId()));
  145 + }
  146 + }
  147 +
109 /** 148 /**
110 * 下载微信媒体文件的内容,并上传到文件服务 149 * 下载微信媒体文件的内容,并上传到文件服务
111 * 150 *
@@ -115,7 +154,7 @@ public class MpMessageServiceImpl implements MpMessageService { @@ -115,7 +154,7 @@ public class MpMessageServiceImpl implements MpMessageService {
115 * @param mediaId 媒体文件编号 154 * @param mediaId 媒体文件编号
116 * @return 上传后的 URL 155 * @return 上传后的 URL
117 */ 156 */
118 - private String mediaDownload(WxMpService mpService, String mediaId) { 157 + private String downloadMedia(WxMpService mpService, String mediaId) {
119 try { 158 try {
120 // 第一步,从公众号下载媒体文件 159 // 第一步,从公众号下载媒体文件
121 File file = mpService.getMaterialService().mediaDownload(mediaId); 160 File file = mpService.getMaterialService().mediaDownload(mediaId);
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/bo/MpMessageSendOutReqBO.java
1 package cn.iocoder.yudao.module.mp.service.message.bo; 1 package cn.iocoder.yudao.module.mp.service.message.bo;
2 2
3 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; 3 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
4 -import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils;  
5 import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*; 4 import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
6 import lombok.Data; 5 import lombok.Data;
7 import me.chanjar.weixin.common.api.WxConsts; 6 import me.chanjar.weixin.common.api.WxConsts;
@@ -36,7 +35,7 @@ public class MpMessageSendOutReqBO { @@ -36,7 +35,7 @@ public class MpMessageSendOutReqBO {
36 /** 35 /**
37 * 消息类型 36 * 消息类型
38 * 37 *
39 - * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS 38 + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC
40 */ 39 */
41 @NotEmpty(message = "消息类型不能为空") 40 @NotEmpty(message = "消息类型不能为空")
42 public String type; 41 public String type;
@@ -56,6 +55,7 @@ public class MpMessageSendOutReqBO { @@ -56,6 +55,7 @@ public class MpMessageSendOutReqBO {
56 */ 55 */
57 @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class}) 56 @NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
58 private String mediaId; 57 private String mediaId;
  58 + // TODO 芋艿:考虑去掉
59 /** 59 /**
60 * 媒体 URL 60 * 媒体 URL
61 * 61 *
@@ -65,6 +65,15 @@ public class MpMessageSendOutReqBO { @@ -65,6 +65,15 @@ public class MpMessageSendOutReqBO {
65 private String mediaUrl; 65 private String mediaUrl;
66 66
67 /** 67 /**
  68 + * 缩略图的媒体 id
  69 + *
  70 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC
  71 + */
  72 + @NotEmpty(message = "消息内容不能为空", groups = {MusicGroup.class})
  73 + private String thumbMediaId;
  74 + // TODO 芋艿:考虑去掉
  75 +
  76 + /**
68 * 标题 77 * 标题
69 * 78 *
70 * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO 79 * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
@@ -88,4 +97,18 @@ public class MpMessageSendOutReqBO { @@ -88,4 +97,18 @@ public class MpMessageSendOutReqBO {
88 @NotNull(message = "图文消息不能为空", groups = NewsGroup.class) 97 @NotNull(message = "图文消息不能为空", groups = NewsGroup.class)
89 private List<MpMessageDO.Article> articles; 98 private List<MpMessageDO.Article> articles;
90 99
  100 + /**
  101 + * 音乐链接
  102 + *
  103 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
  104 + */
  105 + private String musicUrl;
  106 +
  107 + /**
  108 + * 高质量音乐链接
  109 + *
  110 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
  111 + */
  112 + private String hqMusicUrl;
  113 +
91 } 114 }
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; @@ -9,6 +9,9 @@ import me.chanjar.weixin.mp.bean.result.WxMpUser;
9 import java.util.Collection; 9 import java.util.Collection;
10 import java.util.List; 10 import java.util.List;
11 11
  12 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  13 +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.USER_NOT_EXISTS;
  14 +
12 /** 15 /**
13 * 公众号粉丝 Service 接口 16 * 公众号粉丝 Service 接口
14 * 17 *
@@ -34,6 +37,20 @@ public interface MpUserService { @@ -34,6 +37,20 @@ public interface MpUserService {
34 MpUserDO getUser(String appId, String openId); 37 MpUserDO getUser(String appId, String openId);
35 38
36 /** 39 /**
  40 + * 获得公众号粉丝
  41 + *
  42 + * @param id 编号
  43 + * @return 公众号粉丝
  44 + */
  45 + default MpUserDO getRequiredUser(Long id) {
  46 + MpUserDO user = getUser(id);
  47 + if (user == null) {
  48 + throw exception(USER_NOT_EXISTS);
  49 + }
  50 + return user;
  51 + }
  52 +
  53 + /**
37 * 获得公众号粉丝列表 54 * 获得公众号粉丝列表
38 * 55 *
39 * @param ids 编号 56 * @param ids 编号
yudao-ui-admin/src/views/mp/components/wx-msg/main.vue
@@ -140,7 +140,6 @@ @@ -140,7 +140,6 @@
140 nickname: '公众号', 140 nickname: '公众号',
141 avatar: require("@/assets/images/wechat.png"), 141 avatar: require("@/assets/images/wechat.png"),
142 }, 142 },
143 - qqMapKey: '' //  
144 } 143 }
145 }, 144 },
146 created() { 145 created() {