Commit c141ebef3f04e99b17df6b4e8cb80a01bac5947d

Authored by YunaiV
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
@@ -10,7 +10,7 @@ import lombok.Getter; @@ -10,7 +10,7 @@ import lombok.Getter;
10 */ 10 */
11 @Getter 11 @Getter
12 @AllArgsConstructor 12 @AllArgsConstructor
13 -public enum MpMessageSendFrom { 13 +public enum MpMessageSendFromEnum {
14 14
15 USER_TO_MP(1, "用户发送给公众号"), 15 USER_TO_MP(1, "用户发送给公众号"),
16 MP_TO_USER(2, "公众号发给用户"), 16 MP_TO_USER(2, "公众号发给用户"),
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,8 +4,13 @@ import java.util.*;
4 4
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6
  7 +import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
7 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; 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 import org.mapstruct.Mapper; 11 import org.mapstruct.Mapper;
  12 +import org.mapstruct.Mapping;
  13 +import org.mapstruct.Mappings;
9 import org.mapstruct.factory.Mappers; 14 import org.mapstruct.factory.Mappers;
10 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*; 15 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*;
11 16
@@ -20,4 +25,19 @@ public interface MpMessageConvert { @@ -20,4 +25,19 @@ public interface MpMessageConvert {
20 25
21 PageResult<MpMessageRespVO> convertPage(PageResult<MpMessageDO> page); 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 package cn.iocoder.yudao.module.mp.dal.dataobject.message; 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 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; 4 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
5 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; 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 import lombok.*; 10 import lombok.*;
9 -  
10 -import com.baomidou.mybatisplus.annotation.*;  
11 -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;  
12 import me.chanjar.weixin.common.api.WxConsts; 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 * 微信消息 DO 14 * 微信消息 DO
20 * 15 *
@@ -36,6 +31,10 @@ public class MpMessageDO extends BaseDO { @@ -36,6 +31,10 @@ public class MpMessageDO extends BaseDO {
36 @TableId 31 @TableId
37 private Long id; 32 private Long id;
38 /** 33 /**
  34 + * 微信公众号消息 id
  35 + */
  36 + private Long msgId;
  37 + /**
39 * 微信公众号 ID 38 * 微信公众号 ID
40 * 39 *
41 * 关联 {@link MpAccountDO#getId()} 40 * 关联 {@link MpAccountDO#getId()}
@@ -56,17 +55,9 @@ public class MpMessageDO extends BaseDO { @@ -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,11 +68,11 @@ public class MpMessageDO extends BaseDO {
77 /** 68 /**
78 * 消息来源 69 * 消息来源
79 * 70 *
80 - * 枚举 {@link MpMessageSendFrom} 71 + * 枚举 {@link MpMessageSendFromEnum}
81 */ 72 */
82 private Integer sendFrom; 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,50 +88,53 @@ public class MpMessageDO extends BaseDO {
97 */ 88 */
98 private String mediaId; 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 * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id 120 * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
126 * 121 *
127 - * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC 122 + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
128 */ 123 */
129 private String thumbMediaId; 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,47 +154,29 @@ public class MpMessageDO extends BaseDO {
160 * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION 154 * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION
161 */ 155 */
162 private Double scale; 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 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
@@ -13,7 +13,7 @@ public interface MpMessageMapper extends BaseMapperX&lt;MpMessageDO&gt; { @@ -13,7 +13,7 @@ public interface MpMessageMapper extends BaseMapperX&lt;MpMessageDO&gt; {
13 default PageResult<MpMessageDO> selectPage(MpMessagePageReqVO reqVO) { 13 default PageResult<MpMessageDO> selectPage(MpMessagePageReqVO reqVO) {
14 return selectPage(reqVO, new LambdaQueryWrapperX<MpMessageDO>() 14 return selectPage(reqVO, new LambdaQueryWrapperX<MpMessageDO>()
15 .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId()) 15 .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId())
16 - .eqIfPresent(MpMessageDO::getOpenId, reqVO.getOpenId()) 16 + .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenId())
17 // .likeIfPresent(MpMessageDO::getNickname, reqVO.getNickname()) 17 // .likeIfPresent(MpMessageDO::getNickname, reqVO.getNickname())
18 .eqIfPresent(MpMessageDO::getType, reqVO.getType()) 18 .eqIfPresent(MpMessageDO::getType, reqVO.getType())
19 .orderByDesc(MpMessageDO::getId)); 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
1 -package cn.iocoder.yudao.module.mp.dal.mysql.accountfans; 1 +package cn.iocoder.yudao.module.mp.dal.mysql.user;
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.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
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,7 +3,7 @@ package cn.iocoder.yudao.module.mp.framework.mp.config;
3 import cn.iocoder.yudao.module.mp.framework.mp.core.DefaultMpServiceFactory; 3 import cn.iocoder.yudao.module.mp.framework.mp.core.DefaultMpServiceFactory;
4 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; 4 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
5 import cn.iocoder.yudao.module.mp.service.handler.menu.MenuHandler; 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 import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler; 7 import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler;
8 import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler; 8 import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler;
9 import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler; 9 import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler;
@@ -36,7 +36,7 @@ public class MpConfiguration { @@ -36,7 +36,7 @@ public class MpConfiguration {
36 @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") 36 @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
37 public MpServiceFactory mpServiceFactory(RedisTemplateWxRedisOps redisTemplateWxRedisOps, 37 public MpServiceFactory mpServiceFactory(RedisTemplateWxRedisOps redisTemplateWxRedisOps,
38 WxMpProperties wxMpProperties, 38 WxMpProperties wxMpProperties,
39 - MessageLogHandler logHandler, 39 + MessageReceiveHandler logHandler,
40 KfSessionHandler kfSessionHandler, 40 KfSessionHandler kfSessionHandler,
41 StoreCheckNotifyHandler storeCheckNotifyHandler, 41 StoreCheckNotifyHandler storeCheckNotifyHandler,
42 MenuHandler menuHandler, 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,7 +2,7 @@ package cn.iocoder.yudao.module.mp.framework.mp.core;
2 2
3 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; 3 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
4 import cn.iocoder.yudao.module.mp.service.handler.menu.MenuHandler; 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 import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler; 6 import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler;
7 import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler; 7 import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler;
8 import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler; 8 import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler;
@@ -49,7 +49,7 @@ public class DefaultMpServiceFactory implements MpServiceFactory { @@ -49,7 +49,7 @@ public class DefaultMpServiceFactory implements MpServiceFactory {
49 49
50 // ========== 各种 Handler ========== 50 // ========== 各种 Handler ==========
51 51
52 - private final MessageLogHandler logHandler; 52 + private final MessageReceiveHandler logHandler;
53 private final KfSessionHandler kfSessionHandler; 53 private final KfSessionHandler kfSessionHandler;
54 private final StoreCheckNotifyHandler storeCheckNotifyHandler; 54 private final StoreCheckNotifyHandler storeCheckNotifyHandler;
55 private final MenuHandler menuHandler; 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,91 +52,91 @@ public class MessageAutoReplyHandler implements WxMpMessageHandler {
52 log.info("收到信息内容:{}", JsonUtils.toJsonString(wxMessage)); 52 log.info("收到信息内容:{}", JsonUtils.toJsonString(wxMessage));
53 log.info("关键字:{}", wxMessage.getContent()); 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 return new TextBuilder().build("测试", wxMessage, weixinService); 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 package cn.iocoder.yudao.module.mp.service.handler.message; 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 import lombok.extern.slf4j.Slf4j; 5 import lombok.extern.slf4j.Slf4j;
5 import me.chanjar.weixin.common.session.WxSessionManager; 6 import me.chanjar.weixin.common.session.WxSessionManager;
6 import me.chanjar.weixin.mp.api.WxMpMessageHandler; 7 import me.chanjar.weixin.mp.api.WxMpMessageHandler;
@@ -9,21 +10,26 @@ import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; @@ -9,21 +10,26 @@ import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
9 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; 10 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
10 import org.springframework.stereotype.Component; 11 import org.springframework.stereotype.Component;
11 12
  13 +import javax.annotation.Resource;
12 import java.util.Map; 14 import java.util.Map;
13 15
14 /** 16 /**
15 * 保存微信消息的事件处理器 17 * 保存微信消息的事件处理器
16 * 18 *
17 - * // TODO 芋艿:实现一下 19 + * @author 芋道源码
18 */ 20 */
19 @Component 21 @Component
20 @Slf4j 22 @Slf4j
21 -public class MessageLogHandler implements WxMpMessageHandler { 23 +public class MessageReceiveHandler implements WxMpMessageHandler {
  24 +
  25 + @Resource
  26 + private MpMessageService mpMessageService;
22 27
23 @Override 28 @Override
24 public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, 29 public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context,
25 WxMpService wxMpService, WxSessionManager sessionManager) { 30 WxMpService wxMpService, WxSessionManager sessionManager) {
26 - log.info("接收到请求消息,内容:{}", JsonUtils.toJsonString(wxMessage)); 31 + log.info("[handle][接收到请求消息,内容:{}]", wxMessage);
  32 + mpMessageService.createFromUser(MpContextHolder.getAppId(), wxMessage);
27 return null; 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,6 +3,7 @@ package cn.iocoder.yudao.module.mp.service.message;
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.dal.dataobject.message.MpMessageDO; 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 * 粉丝消息表 Service 接口 9 * 粉丝消息表 Service 接口
@@ -12,11 +13,18 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; @@ -12,11 +13,18 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
12 public interface MpMessageService { 13 public interface MpMessageService {
13 14
14 /** 15 /**
15 - * 获得粉丝消息表 分页 16 + * 获得粉丝消息表分页
16 * 17 *
17 * @param pageReqVO 分页查询 18 * @param pageReqVO 分页查询
18 * @return 粉丝消息表 分页 19 * @return 粉丝消息表 分页
19 */ 20 */
20 PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO); 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 package cn.iocoder.yudao.module.mp.service.message; 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 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; 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 import org.springframework.stereotype.Service; 24 import org.springframework.stereotype.Service;
5 25
6 import javax.annotation.Resource; 26 import javax.annotation.Resource;
@@ -10,7 +30,9 @@ import org.springframework.validation.annotation.Validated; @@ -10,7 +30,9 @@ import org.springframework.validation.annotation.Validated;
10 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*; 30 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*;
11 import cn.iocoder.yudao.framework.common.pojo.PageResult; 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 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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,14 +43,77 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
21 */ 43 */
22 @Service 44 @Service
23 @Validated 45 @Validated
  46 +@Slf4j
24 public class MpMessageServiceImpl implements MpMessageService { 47 public class MpMessageServiceImpl implements MpMessageService {
25 48
26 @Resource 49 @Resource
  50 + @Lazy // 延迟加载,避免循环依赖
  51 + private MpAccountService mpAccountService;
  52 + @Resource
  53 + private MpUserService mpUserService;
  54 +
  55 + @Resource
27 private MpMessageMapper mpMessageMapper; 56 private MpMessageMapper mpMessageMapper;
28 57
  58 + @Resource
  59 + @Lazy // 延迟加载,解决循环依赖的问题
  60 + private MpServiceFactory mpServiceFactory;
  61 +
  62 + @Resource
  63 + private FileApi fileApi;
  64 +
29 @Override 65 @Override
30 public PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO) { 66 public PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO) {
31 return mpMessageMapper.selectPage(pageReqVO); 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,6 +24,15 @@ public interface MpUserService {
24 MpUserDO getUser(Long id); 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 * @param ids 编号 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,7 +6,7 @@ import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserPageReqVO;
6 import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert; 6 import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert;
7 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; 7 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
8 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; 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 import cn.iocoder.yudao.module.mp.service.account.MpAccountService; 10 import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
11 import lombok.extern.slf4j.Slf4j; 11 import lombok.extern.slf4j.Slf4j;
12 import me.chanjar.weixin.mp.bean.result.WxMpUser; 12 import me.chanjar.weixin.mp.bean.result.WxMpUser;
@@ -42,6 +42,11 @@ public class MpUserServiceImpl implements MpUserService { @@ -42,6 +42,11 @@ public class MpUserServiceImpl implements MpUserService {
42 } 42 }
43 43
44 @Override 44 @Override
  45 + public MpUserDO getUser(String appId, String openId) {
  46 + return mpUserMapper.selectByAppIdAndOpenid(appId, openId);
  47 + }
  48 +
  49 + @Override
45 public List<MpUserDO> getUserList(Collection<Long> ids) { 50 public List<MpUserDO> getUserList(Collection<Long> ids) {
46 return mpUserMapper.selectBatchIds(ids); 51 return mpUserMapper.selectBatchIds(ids);
47 } 52 }