Commit c141ebef3f04e99b17df6b4e8cb80a01bac5947d
1 parent
fefad029
mp:实现消息的存储
Showing
13 changed files
with
292 additions
and
183 deletions
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFrom.java renamed to yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java
| ... | ... | @@ -4,8 +4,13 @@ import java.util.*; |
| 4 | 4 | |
| 5 | 5 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 6 | 6 | |
| 7 | +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; | |
| 7 | 8 | import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; |
| 9 | +import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; | |
| 10 | +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; | |
| 8 | 11 | import org.mapstruct.Mapper; |
| 12 | +import org.mapstruct.Mapping; | |
| 13 | +import org.mapstruct.Mappings; | |
| 9 | 14 | import org.mapstruct.factory.Mappers; |
| 10 | 15 | import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*; |
| 11 | 16 | |
| ... | ... | @@ -20,4 +25,19 @@ public interface MpMessageConvert { |
| 20 | 25 | |
| 21 | 26 | PageResult<MpMessageRespVO> convertPage(PageResult<MpMessageDO> page); |
| 22 | 27 | |
| 28 | + @Mappings(value = { | |
| 29 | + @Mapping(source = "msgType", target = "type"), | |
| 30 | + }) | |
| 31 | + MpMessageDO convert(WxMpXmlMessage wxMessage); | |
| 32 | + | |
| 33 | + default MpMessageDO convert(WxMpXmlMessage wxMessage, MpAccountDO account, MpUserDO user) { | |
| 34 | + MpMessageDO message = convert(wxMessage); | |
| 35 | + if (account != null) { | |
| 36 | + message.setAccountId(account.getId()).setAppId(account.getAppId()); | |
| 37 | + } | |
| 38 | + if (user != null) { | |
| 39 | + message.setUserId(user.getId()).setOpenid(user.getOpenid()); | |
| 40 | + } | |
| 41 | + return message; | |
| 42 | + } | |
| 23 | 43 | } | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java
| 1 | 1 | package cn.iocoder.yudao.module.mp.dal.dataobject.message; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | |
| 4 | 4 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 5 | 5 | import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; |
| 6 | -import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFrom; | |
| 7 | -import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; | |
| 6 | +import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum; | |
| 7 | +import com.baomidou.mybatisplus.annotation.KeySequence; | |
| 8 | +import com.baomidou.mybatisplus.annotation.TableId; | |
| 9 | +import com.baomidou.mybatisplus.annotation.TableName; | |
| 8 | 10 | import lombok.*; |
| 9 | - | |
| 10 | -import com.baomidou.mybatisplus.annotation.*; | |
| 11 | -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | |
| 12 | 11 | import me.chanjar.weixin.common.api.WxConsts; |
| 13 | 12 | |
| 14 | -import java.io.Serializable; | |
| 15 | -import java.time.LocalDateTime; | |
| 16 | -import java.util.List; | |
| 17 | - | |
| 18 | 13 | /** |
| 19 | 14 | * 微信消息 DO |
| 20 | 15 | * |
| ... | ... | @@ -36,6 +31,10 @@ public class MpMessageDO extends BaseDO { |
| 36 | 31 | @TableId |
| 37 | 32 | private Long id; |
| 38 | 33 | /** |
| 34 | + * 微信公众号消息 id | |
| 35 | + */ | |
| 36 | + private Long msgId; | |
| 37 | + /** | |
| 39 | 38 | * 微信公众号 ID |
| 40 | 39 | * |
| 41 | 40 | * 关联 {@link MpAccountDO#getId()} |
| ... | ... | @@ -56,17 +55,9 @@ public class MpMessageDO extends BaseDO { |
| 56 | 55 | /** |
| 57 | 56 | * 用户标识 |
| 58 | 57 | * |
| 59 | - * 冗余 {@link MpUserDO#getId()} | |
| 60 | - */ | |
| 61 | - private String openId; | |
| 62 | -// /** | |
| 63 | -// * 昵称 | |
| 64 | -// */ | |
| 65 | -// private String nickname; | |
| 66 | -// /** | |
| 67 | -// * 头像 | |
| 68 | -// */ | |
| 69 | -// private String headImageUrl; | |
| 58 | + * 冗余 {@link MpUserDO#getOpenid()} | |
| 59 | + */ | |
| 60 | + private String openid; | |
| 70 | 61 | |
| 71 | 62 | /** |
| 72 | 63 | * 消息类型 |
| ... | ... | @@ -77,11 +68,11 @@ public class MpMessageDO extends BaseDO { |
| 77 | 68 | /** |
| 78 | 69 | * 消息来源 |
| 79 | 70 | * |
| 80 | - * 枚举 {@link MpMessageSendFrom} | |
| 71 | + * 枚举 {@link MpMessageSendFromEnum} | |
| 81 | 72 | */ |
| 82 | 73 | private Integer sendFrom; |
| 83 | 74 | |
| 84 | - // ========= 消息内容 ========== | |
| 75 | + // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html | |
| 85 | 76 | |
| 86 | 77 | /** |
| 87 | 78 | * 消息内容 |
| ... | ... | @@ -97,50 +88,53 @@ public class MpMessageDO extends BaseDO { |
| 97 | 88 | */ |
| 98 | 89 | private String mediaId; |
| 99 | 90 | /** |
| 100 | - * 标题 | |
| 91 | + * 媒体文件的 URL | |
| 92 | + */ | |
| 93 | + private String mediaUrl; | |
| 94 | + /** | |
| 95 | + * 语音识别后文本 | |
| 101 | 96 | * |
| 102 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC | |
| 97 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE | |
| 103 | 98 | */ |
| 104 | - private String title; | |
| 99 | + private String recognition; | |
| 105 | 100 | /** |
| 106 | - * 描述 | |
| 101 | + * 语音格式,如 amr,speex 等 | |
| 107 | 102 | * |
| 108 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC | |
| 103 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE | |
| 109 | 104 | */ |
| 110 | - private String description; | |
| 111 | - | |
| 105 | + private String format; | |
| 112 | 106 | /** |
| 113 | - * 音乐链接 | |
| 107 | + * 标题 | |
| 114 | 108 | * |
| 115 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC | |
| 109 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC、LINK | |
| 116 | 110 | */ |
| 117 | - private String musicURL; | |
| 111 | + private String title; | |
| 118 | 112 | /** |
| 119 | - * 高质量音乐链接,WIFI 环境优先使用该链接播放音乐 | |
| 113 | + * 描述 | |
| 120 | 114 | * |
| 121 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC | |
| 115 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC | |
| 122 | 116 | */ |
| 123 | - private String hqMusicUrl; | |
| 117 | + private String description; | |
| 118 | + | |
| 124 | 119 | /** |
| 125 | 120 | * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id |
| 126 | 121 | * |
| 127 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC | |
| 122 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO | |
| 128 | 123 | */ |
| 129 | 124 | private String thumbMediaId; |
| 130 | - | |
| 131 | 125 | /** |
| 132 | - * 图文消息个数,限制为 10 条以内 | |
| 126 | + * 缩略图的媒体 URL | |
| 133 | 127 | * |
| 134 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS | |
| 128 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO | |
| 135 | 129 | */ |
| 136 | - private Integer articleCount; | |
| 130 | + private String thumbMediaUrl; | |
| 131 | + | |
| 137 | 132 | /** |
| 138 | - * 图文消息数组 | |
| 133 | + * 点击图文消息跳转链接 | |
| 139 | 134 | * |
| 140 | - * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS | |
| 135 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LINK | |
| 141 | 136 | */ |
| 142 | - @TableField(typeHandler = ArticleTypeHandler.class) | |
| 143 | - private List<Article> articles; | |
| 137 | + private String url; | |
| 144 | 138 | |
| 145 | 139 | /** |
| 146 | 140 | * 地理位置维度 |
| ... | ... | @@ -160,47 +154,29 @@ public class MpMessageDO extends BaseDO { |
| 160 | 154 | * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION |
| 161 | 155 | */ |
| 162 | 156 | private Double scale; |
| 163 | - | |
| 164 | 157 | /** |
| 165 | - * 文章 | |
| 158 | + * 详细地址 | |
| 159 | + * | |
| 160 | + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION | |
| 161 | + * | |
| 162 | + * 例如说杨浦区黄兴路 221-4 号临 | |
| 166 | 163 | */ |
| 167 | - @Data | |
| 168 | - public static class Article implements Serializable { | |
| 169 | - | |
| 170 | - /** | |
| 171 | - * 图文消息标题 | |
| 172 | - */ | |
| 173 | - private String title; | |
| 174 | - /** | |
| 175 | - * 图文消息描述 | |
| 176 | - */ | |
| 177 | - private String description; | |
| 178 | - /** | |
| 179 | - * 图片链接 | |
| 180 | - * | |
| 181 | - * 支持JPG、PNG格式,较好的效果为大图 360*200,小图 200*200 | |
| 182 | - */ | |
| 183 | - private String picUrl; | |
| 184 | - /** | |
| 185 | - * 点击图文消息跳转链接 | |
| 186 | - */ | |
| 187 | - private String url; | |
| 164 | + private String label; | |
| 188 | 165 | |
| 189 | - } | |
| 166 | + // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html | |
| 190 | 167 | |
| 191 | - // TODO @芋艿:可以找一些新的思路 | |
| 192 | - public static class ArticleTypeHandler extends AbstractJsonTypeHandler<List<Article>> { | |
| 193 | - | |
| 194 | - @Override | |
| 195 | - protected List<Article> parse(String json) { | |
| 196 | - return JsonUtils.parseArray(json, Article.class); | |
| 197 | - } | |
| 198 | - | |
| 199 | - @Override | |
| 200 | - protected String toJson(List<Article> obj) { | |
| 201 | - return JsonUtils.toJsonString(obj); | |
| 202 | - } | |
| 203 | - | |
| 204 | - } | |
| 168 | + /** | |
| 169 | + * 事件类型 | |
| 170 | + * | |
| 171 | + * 枚举 {@link WxConsts.EventType} | |
| 172 | + */ | |
| 173 | + private String event; | |
| 174 | + /** | |
| 175 | + * 事件 Key | |
| 176 | + * | |
| 177 | + * 1. {@link WxConsts.EventType} 的 SCAN:qrscene_ 为前缀,后面为二维码的参数值 | |
| 178 | + * 2. {@link WxConsts.EventType} 的 CLICK:与自定义菜单接口中 KEY 值对应 | |
| 179 | + */ | |
| 180 | + private String eventKey; | |
| 205 | 181 | |
| 206 | 182 | } | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/fansmsg/MpMessageMapper.java renamed to yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java
| 1 | -package cn.iocoder.yudao.module.mp.dal.mysql.fansmsg; | |
| 1 | +package cn.iocoder.yudao.module.mp.dal.mysql.message; | |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| ... | ... | @@ -13,7 +13,7 @@ public interface MpMessageMapper extends BaseMapperX<MpMessageDO> { |
| 13 | 13 | default PageResult<MpMessageDO> selectPage(MpMessagePageReqVO reqVO) { |
| 14 | 14 | return selectPage(reqVO, new LambdaQueryWrapperX<MpMessageDO>() |
| 15 | 15 | .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId()) |
| 16 | - .eqIfPresent(MpMessageDO::getOpenId, reqVO.getOpenId()) | |
| 16 | + .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenId()) | |
| 17 | 17 | // .likeIfPresent(MpMessageDO::getNickname, reqVO.getNickname()) |
| 18 | 18 | .eqIfPresent(MpMessageDO::getType, reqVO.getType()) |
| 19 | 19 | .orderByDesc(MpMessageDO::getId)); | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/accountfans/MpUserMapper.java renamed to yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/config/MpConfiguration.java
| ... | ... | @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.mp.framework.mp.config; |
| 3 | 3 | import cn.iocoder.yudao.module.mp.framework.mp.core.DefaultMpServiceFactory; |
| 4 | 4 | import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; |
| 5 | 5 | import cn.iocoder.yudao.module.mp.service.handler.menu.MenuHandler; |
| 6 | -import cn.iocoder.yudao.module.mp.service.handler.message.MessageLogHandler; | |
| 6 | +import cn.iocoder.yudao.module.mp.service.handler.message.MessageReceiveHandler; | |
| 7 | 7 | import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler; |
| 8 | 8 | import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler; |
| 9 | 9 | import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler; |
| ... | ... | @@ -36,7 +36,7 @@ public class MpConfiguration { |
| 36 | 36 | @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") |
| 37 | 37 | public MpServiceFactory mpServiceFactory(RedisTemplateWxRedisOps redisTemplateWxRedisOps, |
| 38 | 38 | WxMpProperties wxMpProperties, |
| 39 | - MessageLogHandler logHandler, | |
| 39 | + MessageReceiveHandler logHandler, | |
| 40 | 40 | KfSessionHandler kfSessionHandler, |
| 41 | 41 | StoreCheckNotifyHandler storeCheckNotifyHandler, |
| 42 | 42 | MenuHandler menuHandler, | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java
| ... | ... | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.mp.framework.mp.core; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 4 | 4 | import cn.iocoder.yudao.module.mp.service.handler.menu.MenuHandler; |
| 5 | -import cn.iocoder.yudao.module.mp.service.handler.message.MessageLogHandler; | |
| 5 | +import cn.iocoder.yudao.module.mp.service.handler.message.MessageReceiveHandler; | |
| 6 | 6 | import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler; |
| 7 | 7 | import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler; |
| 8 | 8 | import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler; |
| ... | ... | @@ -49,7 +49,7 @@ public class DefaultMpServiceFactory implements MpServiceFactory { |
| 49 | 49 | |
| 50 | 50 | // ========== 各种 Handler ========== |
| 51 | 51 | |
| 52 | - private final MessageLogHandler logHandler; | |
| 52 | + private final MessageReceiveHandler logHandler; | |
| 53 | 53 | private final KfSessionHandler kfSessionHandler; |
| 54 | 54 | private final StoreCheckNotifyHandler storeCheckNotifyHandler; |
| 55 | 55 | private final MenuHandler menuHandler; | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageAutoReplyHandler.java
| ... | ... | @@ -52,91 +52,91 @@ public class MessageAutoReplyHandler implements WxMpMessageHandler { |
| 52 | 52 | log.info("收到信息内容:{}", JsonUtils.toJsonString(wxMessage)); |
| 53 | 53 | log.info("关键字:{}", wxMessage.getContent()); |
| 54 | 54 | |
| 55 | - if (!wxMessage.getMsgType().equals(WxConsts.XmlMsgType.EVENT)) { | |
| 56 | - //可以选择将消息保存到本地 | |
| 57 | - | |
| 58 | - // 获取微信用户基本信息 | |
| 59 | - try { | |
| 60 | - WxMpUser wxmpUser = weixinService.getUserService() | |
| 61 | - .userInfo(wxMessage.getFromUser(), null); | |
| 62 | - if (wxmpUser != null) { | |
| 63 | - MpAccountDO wxAccount = mpAccountService.findBy(MpAccountDO::getAccount, wxMessage.getToUser()); | |
| 64 | - if (wxAccount != null) { | |
| 65 | - | |
| 66 | - if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.TEXT)) { | |
| 67 | - WxFansMsgCreateReqVO wxFansMsg = new WxFansMsgCreateReqVO(); | |
| 68 | - wxFansMsg.setOpenid(wxmpUser.getOpenId()); | |
| 69 | - try { | |
| 70 | - wxFansMsg.setNickname(wxmpUser.getNickname().getBytes("UTF-8")); | |
| 71 | - } catch (UnsupportedEncodingException e) { | |
| 72 | - e.printStackTrace(); | |
| 73 | - } | |
| 74 | - wxFansMsg.setHeadimgUrl(wxmpUser.getHeadImgUrl()); | |
| 75 | - wxFansMsg.setWxAccountId(String.valueOf(wxAccount.getId())); | |
| 76 | - wxFansMsg.setMsgType(wxMessage.getMsgType()); | |
| 77 | - wxFansMsg.setContent(wxMessage.getContent()); | |
| 78 | - wxFansMsg.setIsRes("1"); | |
| 79 | - | |
| 80 | - //组装回复消息 | |
| 81 | - String content = processContent(wxMessage); | |
| 82 | - content = HtmlUtil.escape(content); | |
| 83 | - wxFansMsg.setResContent(content); | |
| 84 | - | |
| 85 | - mpMessageService.createWxFansMsg(wxFansMsg); | |
| 86 | - return new TextBuilder().build(content, wxMessage, weixinService); | |
| 87 | - | |
| 88 | - } | |
| 89 | - if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.IMAGE)) { | |
| 90 | - WxFansMsgCreateReqVO wxFansMsg = new WxFansMsgCreateReqVO(); | |
| 91 | - wxFansMsg.setOpenid(wxmpUser.getOpenId()); | |
| 92 | - try { | |
| 93 | - wxFansMsg.setNickname(wxmpUser.getNickname().getBytes("UTF-8")); | |
| 94 | - } catch (UnsupportedEncodingException e) { | |
| 95 | - e.printStackTrace(); | |
| 96 | - } | |
| 97 | - wxFansMsg.setHeadimgUrl(wxmpUser.getHeadImgUrl()); | |
| 98 | - wxFansMsg.setWxAccountId(String.valueOf(wxAccount.getId())); | |
| 99 | - wxFansMsg.setMsgType(wxMessage.getMsgType()); | |
| 100 | - wxFansMsg.setMediaId(wxMessage.getMediaId()); | |
| 101 | - wxFansMsg.setPicUrl(wxMessage.getPicUrl()); | |
| 102 | - String downloadDirStr = fileApi.createFile(HttpUtil.downloadBytes(wxMessage.getPicUrl())); | |
| 103 | - File downloadDir = new File(downloadDirStr); | |
| 104 | - if (!downloadDir.exists()) { | |
| 105 | - downloadDir.mkdirs(); | |
| 106 | - } | |
| 107 | - String filepath = downloadDirStr + System.currentTimeMillis() + ".png"; | |
| 108 | - //微信pic url下载到本地,防止失效 | |
| 109 | - long size = HttpUtil.downloadFile(wxMessage.getPicUrl(), FileUtil.file(filepath)); | |
| 110 | - log.info("download pic size : {}", size); | |
| 111 | - wxFansMsg.setPicPath(filepath); | |
| 112 | - wxFansMsg.setIsRes("0"); | |
| 113 | - mpMessageService.createWxFansMsg(wxFansMsg); | |
| 114 | - } | |
| 115 | - | |
| 116 | - } | |
| 117 | - } | |
| 118 | - } catch (WxErrorException e) { | |
| 119 | - if (e.getError().getErrorCode() == 48001) { | |
| 120 | - log.info("该公众号没有获取用户信息权限!"); | |
| 121 | - } | |
| 122 | - } catch (Exception e) { | |
| 123 | - e.printStackTrace(); | |
| 124 | - } | |
| 125 | - | |
| 126 | - } | |
| 127 | - | |
| 128 | - //当用户输入关键词如“你好”,“客服”等,并且有客服在线时,把消息转发给在线客服 | |
| 129 | - try { | |
| 130 | - if (StringUtils.startsWithAny(wxMessage.getContent(), "你好", "客服") | |
| 131 | - && weixinService.getKefuService().kfOnlineList() | |
| 132 | - .getKfOnlineList().size() > 0) { | |
| 133 | - return WxMpXmlOutMessage.TRANSFER_CUSTOMER_SERVICE() | |
| 134 | - .fromUser(wxMessage.getToUser()) | |
| 135 | - .toUser(wxMessage.getFromUser()).build(); | |
| 136 | - } | |
| 137 | - } catch (WxErrorException e) { | |
| 138 | - e.printStackTrace(); | |
| 139 | - } | |
| 55 | +// if (!wxMessage.getMsgType().equals(WxConsts.XmlMsgType.EVENT)) { | |
| 56 | +// //可以选择将消息保存到本地 | |
| 57 | +// | |
| 58 | +// // 获取微信用户基本信息 | |
| 59 | +// try { | |
| 60 | +// WxMpUser wxmpUser = weixinService.getUserService() | |
| 61 | +// .userInfo(wxMessage.getFromUser(), null); | |
| 62 | +// if (wxmpUser != null) { | |
| 63 | +// MpAccountDO wxAccount = mpAccountService.findBy(MpAccountDO::getAccount, wxMessage.getToUser()); | |
| 64 | +// if (wxAccount != null) { | |
| 65 | +// | |
| 66 | +// if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.TEXT)) { | |
| 67 | +// WxFansMsgCreateReqVO wxFansMsg = new WxFansMsgCreateReqVO(); | |
| 68 | +// wxFansMsg.setOpenid(wxmpUser.getOpenId()); | |
| 69 | +// try { | |
| 70 | +// wxFansMsg.setNickname(wxmpUser.getNickname().getBytes("UTF-8")); | |
| 71 | +// } catch (UnsupportedEncodingException e) { | |
| 72 | +// e.printStackTrace(); | |
| 73 | +// } | |
| 74 | +// wxFansMsg.setHeadimgUrl(wxmpUser.getHeadImgUrl()); | |
| 75 | +// wxFansMsg.setWxAccountId(String.valueOf(wxAccount.getId())); | |
| 76 | +// wxFansMsg.setMsgType(wxMessage.getMsgType()); | |
| 77 | +// wxFansMsg.setContent(wxMessage.getContent()); | |
| 78 | +// wxFansMsg.setIsRes("1"); | |
| 79 | +// | |
| 80 | +// //组装回复消息 | |
| 81 | +// String content = processContent(wxMessage); | |
| 82 | +// content = HtmlUtil.escape(content); | |
| 83 | +// wxFansMsg.setResContent(content); | |
| 84 | +// | |
| 85 | +// mpMessageService.createWxFansMsg(wxFansMsg); | |
| 86 | +// return new TextBuilder().build(content, wxMessage, weixinService); | |
| 87 | +// | |
| 88 | +// } | |
| 89 | +// if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.IMAGE)) { | |
| 90 | +// WxFansMsgCreateReqVO wxFansMsg = new WxFansMsgCreateReqVO(); | |
| 91 | +// wxFansMsg.setOpenid(wxmpUser.getOpenId()); | |
| 92 | +// try { | |
| 93 | +// wxFansMsg.setNickname(wxmpUser.getNickname().getBytes("UTF-8")); | |
| 94 | +// } catch (UnsupportedEncodingException e) { | |
| 95 | +// e.printStackTrace(); | |
| 96 | +// } | |
| 97 | +// wxFansMsg.setHeadimgUrl(wxmpUser.getHeadImgUrl()); | |
| 98 | +// wxFansMsg.setWxAccountId(String.valueOf(wxAccount.getId())); | |
| 99 | +// wxFansMsg.setMsgType(wxMessage.getMsgType()); | |
| 100 | +// wxFansMsg.setMediaId(wxMessage.getMediaId()); | |
| 101 | +// wxFansMsg.setPicUrl(wxMessage.getPicUrl()); | |
| 102 | +// String downloadDirStr = fileApi.createFile(HttpUtil.downloadBytes(wxMessage.getPicUrl())); | |
| 103 | +// File downloadDir = new File(downloadDirStr); | |
| 104 | +// if (!downloadDir.exists()) { | |
| 105 | +// downloadDir.mkdirs(); | |
| 106 | +// } | |
| 107 | +// String filepath = downloadDirStr + System.currentTimeMillis() + ".png"; | |
| 108 | +// //微信pic url下载到本地,防止失效 | |
| 109 | +// long size = HttpUtil.downloadFile(wxMessage.getPicUrl(), FileUtil.file(filepath)); | |
| 110 | +// log.info("download pic size : {}", size); | |
| 111 | +// wxFansMsg.setPicPath(filepath); | |
| 112 | +// wxFansMsg.setIsRes("0"); | |
| 113 | +// mpMessageService.createWxFansMsg(wxFansMsg); | |
| 114 | +// } | |
| 115 | +// | |
| 116 | +// } | |
| 117 | +// } | |
| 118 | +// } catch (WxErrorException e) { | |
| 119 | +// if (e.getError().getErrorCode() == 48001) { | |
| 120 | +// log.info("该公众号没有获取用户信息权限!"); | |
| 121 | +// } | |
| 122 | +// } catch (Exception e) { | |
| 123 | +// e.printStackTrace(); | |
| 124 | +// } | |
| 125 | +// | |
| 126 | +// } | |
| 127 | +// | |
| 128 | +// //当用户输入关键词如“你好”,“客服”等,并且有客服在线时,把消息转发给在线客服 | |
| 129 | +// try { | |
| 130 | +// if (StringUtils.startsWithAny(wxMessage.getContent(), "你好", "客服") | |
| 131 | +// && weixinService.getKefuService().kfOnlineList() | |
| 132 | +// .getKfOnlineList().size() > 0) { | |
| 133 | +// return WxMpXmlOutMessage.TRANSFER_CUSTOMER_SERVICE() | |
| 134 | +// .fromUser(wxMessage.getToUser()) | |
| 135 | +// .toUser(wxMessage.getFromUser()).build(); | |
| 136 | +// } | |
| 137 | +// } catch (WxErrorException e) { | |
| 138 | +// e.printStackTrace(); | |
| 139 | +// } | |
| 140 | 140 | |
| 141 | 141 | //组装默认回复消息 |
| 142 | 142 | return new TextBuilder().build("测试", wxMessage, weixinService); | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageLogHandler.java renamed to yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java
| 1 | 1 | package cn.iocoder.yudao.module.mp.service.handler.message; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 3 | +import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; | |
| 4 | +import cn.iocoder.yudao.module.mp.service.message.MpMessageService; | |
| 4 | 5 | import lombok.extern.slf4j.Slf4j; |
| 5 | 6 | import me.chanjar.weixin.common.session.WxSessionManager; |
| 6 | 7 | import me.chanjar.weixin.mp.api.WxMpMessageHandler; |
| ... | ... | @@ -9,21 +10,26 @@ import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; |
| 9 | 10 | import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; |
| 10 | 11 | import org.springframework.stereotype.Component; |
| 11 | 12 | |
| 13 | +import javax.annotation.Resource; | |
| 12 | 14 | import java.util.Map; |
| 13 | 15 | |
| 14 | 16 | /** |
| 15 | 17 | * 保存微信消息的事件处理器 |
| 16 | 18 | * |
| 17 | - * // TODO 芋艿:实现一下 | |
| 19 | + * @author 芋道源码 | |
| 18 | 20 | */ |
| 19 | 21 | @Component |
| 20 | 22 | @Slf4j |
| 21 | -public class MessageLogHandler implements WxMpMessageHandler { | |
| 23 | +public class MessageReceiveHandler implements WxMpMessageHandler { | |
| 24 | + | |
| 25 | + @Resource | |
| 26 | + private MpMessageService mpMessageService; | |
| 22 | 27 | |
| 23 | 28 | @Override |
| 24 | 29 | public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, |
| 25 | 30 | WxMpService wxMpService, WxSessionManager sessionManager) { |
| 26 | - log.info("接收到请求消息,内容:{}", JsonUtils.toJsonString(wxMessage)); | |
| 31 | + log.info("[handle][接收到请求消息,内容:{}]", wxMessage); | |
| 32 | + mpMessageService.createFromUser(MpContextHolder.getAppId(), wxMessage); | |
| 27 | 33 | return null; |
| 28 | 34 | } |
| 29 | 35 | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java
| ... | ... | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.mp.service.message; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO; |
| 5 | 5 | import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; |
| 6 | +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; | |
| 6 | 7 | |
| 7 | 8 | /** |
| 8 | 9 | * 粉丝消息表 Service 接口 |
| ... | ... | @@ -12,11 +13,18 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; |
| 12 | 13 | public interface MpMessageService { |
| 13 | 14 | |
| 14 | 15 | /** |
| 15 | - * 获得粉丝消息表 分页 | |
| 16 | + * 获得粉丝消息表分页 | |
| 16 | 17 | * |
| 17 | 18 | * @param pageReqVO 分页查询 |
| 18 | 19 | * @return 粉丝消息表 分页 |
| 19 | 20 | */ |
| 20 | 21 | PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO); |
| 21 | 22 | |
| 23 | + /** | |
| 24 | + * 保存粉丝消息,来自用户发送 | |
| 25 | + * | |
| 26 | + * @param appId 微信公众号 appId | |
| 27 | + * @param wxMessage 消息 | |
| 28 | + */ | |
| 29 | + void createFromUser(String appId, WxMpXmlMessage wxMessage); | |
| 22 | 30 | } | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.mp.service.message; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.io.FileTypeUtil; | |
| 4 | +import cn.hutool.core.io.FileUtil; | |
| 5 | +import cn.hutool.core.io.IoUtil; | |
| 6 | +import cn.hutool.core.lang.Assert; | |
| 7 | +import cn.hutool.core.util.StrUtil; | |
| 8 | +import cn.iocoder.yudao.framework.common.util.io.FileUtils; | |
| 9 | +import cn.iocoder.yudao.module.infra.api.file.FileApi; | |
| 10 | +import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert; | |
| 11 | +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; | |
| 3 | 12 | import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; |
| 13 | +import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; | |
| 14 | +import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum; | |
| 15 | +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; | |
| 16 | +import cn.iocoder.yudao.module.mp.service.account.MpAccountService; | |
| 17 | +import cn.iocoder.yudao.module.mp.service.user.MpUserService; | |
| 18 | +import lombok.SneakyThrows; | |
| 19 | +import lombok.extern.slf4j.Slf4j; | |
| 20 | +import me.chanjar.weixin.common.error.WxErrorException; | |
| 21 | +import me.chanjar.weixin.mp.api.WxMpService; | |
| 22 | +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; | |
| 23 | +import org.springframework.context.annotation.Lazy; | |
| 4 | 24 | import org.springframework.stereotype.Service; |
| 5 | 25 | |
| 6 | 26 | import javax.annotation.Resource; |
| ... | ... | @@ -10,7 +30,9 @@ import org.springframework.validation.annotation.Validated; |
| 10 | 30 | import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*; |
| 11 | 31 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 12 | 32 | |
| 13 | -import cn.iocoder.yudao.module.mp.dal.mysql.fansmsg.MpMessageMapper; | |
| 33 | +import cn.iocoder.yudao.module.mp.dal.mysql.message.MpMessageMapper; | |
| 34 | + | |
| 35 | +import java.io.File; | |
| 14 | 36 | |
| 15 | 37 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 16 | 38 | |
| ... | ... | @@ -21,14 +43,77 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU |
| 21 | 43 | */ |
| 22 | 44 | @Service |
| 23 | 45 | @Validated |
| 46 | +@Slf4j | |
| 24 | 47 | public class MpMessageServiceImpl implements MpMessageService { |
| 25 | 48 | |
| 26 | 49 | @Resource |
| 50 | + @Lazy // 延迟加载,避免循环依赖 | |
| 51 | + private MpAccountService mpAccountService; | |
| 52 | + @Resource | |
| 53 | + private MpUserService mpUserService; | |
| 54 | + | |
| 55 | + @Resource | |
| 27 | 56 | private MpMessageMapper mpMessageMapper; |
| 28 | 57 | |
| 58 | + @Resource | |
| 59 | + @Lazy // 延迟加载,解决循环依赖的问题 | |
| 60 | + private MpServiceFactory mpServiceFactory; | |
| 61 | + | |
| 62 | + @Resource | |
| 63 | + private FileApi fileApi; | |
| 64 | + | |
| 29 | 65 | @Override |
| 30 | 66 | public PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO) { |
| 31 | 67 | return mpMessageMapper.selectPage(pageReqVO); |
| 32 | 68 | } |
| 33 | 69 | |
| 70 | + @Override | |
| 71 | + public void createFromUser(String appId, WxMpXmlMessage wxMessage) { | |
| 72 | + WxMpService mpService = mpServiceFactory.getRequiredMpService(appId); | |
| 73 | + // 获得关联信息 | |
| 74 | + MpAccountDO account = mpAccountService.getAccountFromCache(appId); | |
| 75 | + Assert.notNull(account, "公众号账号({}) 不存在", appId); | |
| 76 | + MpUserDO user = mpUserService.getUser(appId, wxMessage.getFromUser()); | |
| 77 | + Assert.notNull(account, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser()); | |
| 78 | + | |
| 79 | + // 记录消息 | |
| 80 | + MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user); | |
| 81 | + message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom()); | |
| 82 | + if (StrUtil.isNotEmpty(message.getMediaId())) { | |
| 83 | + message.setMediaUrl(mediaDownload(mpService, message.getMediaId())); | |
| 84 | + } | |
| 85 | + if (StrUtil.isNotEmpty(message.getThumbMediaId())) { | |
| 86 | + message.setThumbMediaUrl(mediaDownload(mpService, message.getThumbMediaId())); | |
| 87 | + } | |
| 88 | + mpMessageMapper.insert(message); | |
| 89 | + | |
| 90 | +// WxConsts.MenuButtonType.VIEW | |
| 91 | +// wxMessage.getEventKey() | |
| 92 | + | |
| 93 | +// WxConsts.MenuButtonType.CLICK | |
| 94 | +// wxMessage.getEventKey() | |
| 95 | + } | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * 下载微信媒体文件的内容,并上传到文件服务 | |
| 99 | + * | |
| 100 | + * 为什么要下载?媒体文件在微信后台保存时间为 3 天,即 3 天后 media_id 失效。 | |
| 101 | + * | |
| 102 | + * @param mpService 微信公众号 Service | |
| 103 | + * @param mediaId 媒体文件编号 | |
| 104 | + * @return 上传后的 URL | |
| 105 | + */ | |
| 106 | + private String mediaDownload(WxMpService mpService, String mediaId) { | |
| 107 | + try { | |
| 108 | + // 第一步,从公众号下载媒体文件 | |
| 109 | + File file = mpService.getMaterialService().mediaDownload(mediaId); | |
| 110 | + // 第二步,上传到文件服务 | |
| 111 | + String path = mediaId + "." + FileTypeUtil.getType(file); | |
| 112 | + return fileApi.createFile(path, FileUtil.readBytes(file)); | |
| 113 | + } catch (WxErrorException e) { | |
| 114 | + log.error("[mediaDownload][media({}) 下载失败]", mediaId); | |
| 115 | + } | |
| 116 | + return null; | |
| 117 | + } | |
| 118 | + | |
| 34 | 119 | } | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserService.java
| ... | ... | @@ -24,6 +24,15 @@ public interface MpUserService { |
| 24 | 24 | MpUserDO getUser(Long id); |
| 25 | 25 | |
| 26 | 26 | /** |
| 27 | + * 使用 appId + openId,获得微信公众号粉丝 | |
| 28 | + * | |
| 29 | + * @param appId 微信公众号 appId | |
| 30 | + * @param openId 微信公众号 openId | |
| 31 | + * @return 微信公众号粉丝 | |
| 32 | + */ | |
| 33 | + MpUserDO getUser(String appId, String openId); | |
| 34 | + | |
| 35 | + /** | |
| 27 | 36 | * 获得微信公众号粉丝列表 |
| 28 | 37 | * |
| 29 | 38 | * @param ids 编号 | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserServiceImpl.java
| ... | ... | @@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserPageReqVO; |
| 6 | 6 | import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert; |
| 7 | 7 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 8 | 8 | import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; |
| 9 | -import cn.iocoder.yudao.module.mp.dal.mysql.accountfans.MpUserMapper; | |
| 9 | +import cn.iocoder.yudao.module.mp.dal.mysql.user.MpUserMapper; | |
| 10 | 10 | import cn.iocoder.yudao.module.mp.service.account.MpAccountService; |
| 11 | 11 | import lombok.extern.slf4j.Slf4j; |
| 12 | 12 | import me.chanjar.weixin.mp.bean.result.WxMpUser; |
| ... | ... | @@ -42,6 +42,11 @@ public class MpUserServiceImpl implements MpUserService { |
| 42 | 42 | } |
| 43 | 43 | |
| 44 | 44 | @Override |
| 45 | + public MpUserDO getUser(String appId, String openId) { | |
| 46 | + return mpUserMapper.selectByAppIdAndOpenid(appId, openId); | |
| 47 | + } | |
| 48 | + | |
| 49 | + @Override | |
| 45 | 50 | public List<MpUserDO> getUserList(Collection<Long> ids) { |
| 46 | 51 | return mpUserMapper.selectBatchIds(ids); |
| 47 | 52 | } | ... | ... |