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 10 */
11 11 @Getter
12 12 @AllArgsConstructor
13   -public enum MpMessageSendFrom {
  13 +public enum MpMessageSendFromEnum {
14 14  
15 15 USER_TO_MP(1, "用户发送给公众号"),
16 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 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&lt;MpMessageDO&gt; {
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
1   -package cn.iocoder.yudao.module.mp.dal.mysql.accountfans;
  1 +package cn.iocoder.yudao.module.mp.dal.mysql.user;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 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 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 }
... ...