Commit 9cf88d1929abe9fc24124d41fdc3119d30e30da8
1 parent
f8eed5dd
mp:后端增加发送客服消息接口
Showing
11 changed files
with
295 additions
and
30 deletions
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