Forráskód Böngészése

mp:实现消息的存储

YunaiV 2 éve
szülő
commit
c141ebef3f
13 módosított fájl, 292 hozzáadás és 183 törlés
  1. 1 1
      yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFromEnum.java
  2. 20 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java
  3. 59 83
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java
  4. 2 2
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java
  5. 1 1
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java
  6. 2 2
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/config/MpConfiguration.java
  7. 2 2
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java
  8. 85 85
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageAutoReplyHandler.java
  9. 10 4
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java
  10. 9 1
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java
  11. 86 1
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java
  12. 9 0
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserService.java
  13. 6 1
      yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserServiceImpl.java

+ 1 - 1
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpMessageSendFrom.java → 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;
  */
 @Getter
 @AllArgsConstructor
-public enum MpMessageSendFrom {
+public enum MpMessageSendFromEnum {
 
     USER_TO_MP(1, "用户发送给公众号"),
     MP_TO_USER(2, "公众号发给用户"),

+ 20 - 0
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.*;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*;
 
@@ -20,4 +25,19 @@ public interface MpMessageConvert {
 
     PageResult<MpMessageRespVO> convertPage(PageResult<MpMessageDO> page);
 
+    @Mappings(value = {
+            @Mapping(source = "msgType", target = "type"),
+    })
+    MpMessageDO convert(WxMpXmlMessage wxMessage);
+
+    default MpMessageDO convert(WxMpXmlMessage wxMessage, MpAccountDO account, MpUserDO user) {
+        MpMessageDO message = convert(wxMessage);
+        if (account != null) {
+            message.setAccountId(account.getId()).setAppId(account.getAppId());
+        }
+        if (user != null) {
+            message.setUserId(user.getId()).setOpenid(user.getOpenid());
+        }
+        return message;
+    }
 }

+ 59 - 83
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpMessageDO.java

@@ -1,20 +1,15 @@
 package cn.iocoder.yudao.module.mp.dal.dataobject.message;
 
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
-import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFrom;
-import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
+import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
-
-import com.baomidou.mybatisplus.annotation.*;
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import me.chanjar.weixin.common.api.WxConsts;
 
-import java.io.Serializable;
-import java.time.LocalDateTime;
-import java.util.List;
-
 /**
  * 微信消息 DO
  *
@@ -35,6 +30,10 @@ public class MpMessageDO extends BaseDO {
      */
     @TableId
     private Long id;
+    /**
+     * 微信公众号消息 id
+     */
+    private Long msgId;
     /**
      * 微信公众号 ID
      *
@@ -56,17 +55,9 @@ public class MpMessageDO extends BaseDO {
     /**
      * 用户标识
      *
-     * 冗余 {@link MpUserDO#getId()}
-     */
-    private String openId;
-//    /**
-//     * 昵称
-//     */
-//    private String nickname;
-//    /**
-//     * 头像
-//     */
-//    private String headImageUrl;
+     * 冗余 {@link MpUserDO#getOpenid()}
+     */
+    private String openid;
 
     /**
      * 消息类型
@@ -77,11 +68,11 @@ public class MpMessageDO extends BaseDO {
     /**
      * 消息来源
      *
-     * 枚举 {@link MpMessageSendFrom}
+     * 枚举 {@link MpMessageSendFromEnum}
      */
     private Integer sendFrom;
 
-    // ========= 消息内容 ==========
+    // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html
 
     /**
      * 消息内容
@@ -97,50 +88,53 @@ public class MpMessageDO extends BaseDO {
      */
     private String mediaId;
     /**
-     * 标题
+     * 媒体文件的 URL
+     */
+    private String mediaUrl;
+    /**
+     * 语音识别后文本
      *
-     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE
      */
-    private String title;
+    private String recognition;
     /**
-     * 描述
+     * 语音格式,如 amr,speex 等
      *
-     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE
      */
-    private String description;
-
+    private String format;
     /**
-     * 音乐链接
+     * 标题
      *
-     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC、LINK
      */
-    private String musicURL;
+    private String title;
     /**
-     * 高质量音乐链接,WIFI 环境优先使用该链接播放音乐
+     * 描述
      *
-     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC
      */
-    private String hqMusicUrl;
+    private String description;
+
     /**
      * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id
      *
-     * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
      */
     private String thumbMediaId;
-
     /**
-     * 图文消息个数,限制为 10 条以内
+     * 缩略图的媒体 URL
      *
-     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
      */
-    private Integer articleCount;
+    private String thumbMediaUrl;
+
     /**
-     * 图文消息数组
+     * 点击图文消息跳转链接
      *
-     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 LINK
      */
-    @TableField(typeHandler = ArticleTypeHandler.class)
-    private List<Article> articles;
+    private String url;
 
     /**
      * 地理位置维度
@@ -160,47 +154,29 @@ public class MpMessageDO extends BaseDO {
      * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION
      */
     private Double scale;
-
     /**
-     * 文章
+     * 详细地址
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION
+     *
+     * 例如说杨浦区黄兴路 221-4 号临
      */
-    @Data
-    public static class Article implements Serializable {
-
-        /**
-         * 图文消息标题
-         */
-        private String title;
-        /**
-         * 图文消息描述
-         */
-        private String description;
-        /**
-         * 图片链接
-         *
-         * 支持JPG、PNG格式,较好的效果为大图 360*200,小图 200*200
-         */
-        private String picUrl;
-        /**
-         * 点击图文消息跳转链接
-         */
-        private String url;
+    private String label;
 
-    }
+    // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
 
-    // TODO @芋艿:可以找一些新的思路
-    public static class ArticleTypeHandler extends AbstractJsonTypeHandler<List<Article>> {
-
-        @Override
-        protected List<Article> parse(String json) {
-            return JsonUtils.parseArray(json, Article.class);
-        }
-
-        @Override
-        protected String toJson(List<Article> obj) {
-            return JsonUtils.toJsonString(obj);
-        }
-
-    }
+    /**
+     * 事件类型
+     *
+     * 枚举 {@link WxConsts.EventType}
+     */
+    private String event;
+    /**
+     * 事件 Key
+     *
+     * 1. {@link WxConsts.EventType} 的 SCAN:qrscene_ 为前缀,后面为二维码的参数值
+     * 2. {@link WxConsts.EventType} 的 CLICK:与自定义菜单接口中 KEY 值对应
+     */
+    private String eventKey;
 
 }

+ 2 - 2
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/fansmsg/MpMessageMapper.java → yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.mp.dal.mysql.fansmsg;
+package cn.iocoder.yudao.module.mp.dal.mysql.message;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
@@ -13,7 +13,7 @@ public interface MpMessageMapper extends BaseMapperX<MpMessageDO> {
     default PageResult<MpMessageDO> selectPage(MpMessagePageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<MpMessageDO>()
                 .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId())
-                .eqIfPresent(MpMessageDO::getOpenId, reqVO.getOpenId())
+                .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenId())
 //                .likeIfPresent(MpMessageDO::getNickname, reqVO.getNickname())
                 .eqIfPresent(MpMessageDO::getType, reqVO.getType())
                 .orderByDesc(MpMessageDO::getId));

+ 1 - 1
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/accountfans/MpUserMapper.java → yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.mp.dal.mysql.accountfans;
+package cn.iocoder.yudao.module.mp.dal.mysql.user;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;

+ 2 - 2
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;
 import cn.iocoder.yudao.module.mp.framework.mp.core.DefaultMpServiceFactory;
 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
 import cn.iocoder.yudao.module.mp.service.handler.menu.MenuHandler;
-import cn.iocoder.yudao.module.mp.service.handler.message.MessageLogHandler;
+import cn.iocoder.yudao.module.mp.service.handler.message.MessageReceiveHandler;
 import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler;
 import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler;
 import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler;
@@ -36,7 +36,7 @@ public class MpConfiguration {
     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
     public MpServiceFactory mpServiceFactory(RedisTemplateWxRedisOps redisTemplateWxRedisOps,
                                              WxMpProperties wxMpProperties,
-                                             MessageLogHandler logHandler,
+                                             MessageReceiveHandler logHandler,
                                              KfSessionHandler kfSessionHandler,
                                              StoreCheckNotifyHandler storeCheckNotifyHandler,
                                              MenuHandler menuHandler,

+ 2 - 2
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;
 
 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.service.handler.menu.MenuHandler;
-import cn.iocoder.yudao.module.mp.service.handler.message.MessageLogHandler;
+import cn.iocoder.yudao.module.mp.service.handler.message.MessageReceiveHandler;
 import cn.iocoder.yudao.module.mp.service.handler.message.MessageAutoReplyHandler;
 import cn.iocoder.yudao.module.mp.service.handler.other.KfSessionHandler;
 import cn.iocoder.yudao.module.mp.service.handler.other.NullHandler;
@@ -49,7 +49,7 @@ public class DefaultMpServiceFactory implements MpServiceFactory {
 
     // ========== 各种 Handler ==========
 
-    private final MessageLogHandler logHandler;
+    private final MessageReceiveHandler logHandler;
     private final KfSessionHandler kfSessionHandler;
     private final StoreCheckNotifyHandler storeCheckNotifyHandler;
     private final MenuHandler menuHandler;

+ 85 - 85
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 {
         log.info("收到信息内容:{}", JsonUtils.toJsonString(wxMessage));
         log.info("关键字:{}", wxMessage.getContent());
 
-        if (!wxMessage.getMsgType().equals(WxConsts.XmlMsgType.EVENT)) {
-            //可以选择将消息保存到本地
-
-            // 获取微信用户基本信息
-            try {
-                WxMpUser wxmpUser = weixinService.getUserService()
-                        .userInfo(wxMessage.getFromUser(), null);
-                if (wxmpUser != null) {
-                    MpAccountDO wxAccount = mpAccountService.findBy(MpAccountDO::getAccount, wxMessage.getToUser());
-                    if (wxAccount != null) {
-
-                        if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.TEXT)) {
-                            WxFansMsgCreateReqVO wxFansMsg = new WxFansMsgCreateReqVO();
-                            wxFansMsg.setOpenid(wxmpUser.getOpenId());
-                            try {
-                                wxFansMsg.setNickname(wxmpUser.getNickname().getBytes("UTF-8"));
-                            } catch (UnsupportedEncodingException e) {
-                                e.printStackTrace();
-                            }
-                            wxFansMsg.setHeadimgUrl(wxmpUser.getHeadImgUrl());
-                            wxFansMsg.setWxAccountId(String.valueOf(wxAccount.getId()));
-                            wxFansMsg.setMsgType(wxMessage.getMsgType());
-                            wxFansMsg.setContent(wxMessage.getContent());
-                            wxFansMsg.setIsRes("1");
-
-                            //组装回复消息
-                            String content = processContent(wxMessage);
-                            content = HtmlUtil.escape(content);
-                            wxFansMsg.setResContent(content);
-
-                            mpMessageService.createWxFansMsg(wxFansMsg);
-                            return new TextBuilder().build(content, wxMessage, weixinService);
-
-                        }
-                        if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.IMAGE)) {
-                            WxFansMsgCreateReqVO wxFansMsg = new WxFansMsgCreateReqVO();
-                            wxFansMsg.setOpenid(wxmpUser.getOpenId());
-                            try {
-                                wxFansMsg.setNickname(wxmpUser.getNickname().getBytes("UTF-8"));
-                            } catch (UnsupportedEncodingException e) {
-                                e.printStackTrace();
-                            }
-                            wxFansMsg.setHeadimgUrl(wxmpUser.getHeadImgUrl());
-                            wxFansMsg.setWxAccountId(String.valueOf(wxAccount.getId()));
-                            wxFansMsg.setMsgType(wxMessage.getMsgType());
-                            wxFansMsg.setMediaId(wxMessage.getMediaId());
-                            wxFansMsg.setPicUrl(wxMessage.getPicUrl());
-                            String downloadDirStr = fileApi.createFile(HttpUtil.downloadBytes(wxMessage.getPicUrl()));
-                            File downloadDir = new File(downloadDirStr);
-                            if (!downloadDir.exists()) {
-                                downloadDir.mkdirs();
-                            }
-                            String filepath = downloadDirStr + System.currentTimeMillis() + ".png";
-                            //微信pic url下载到本地,防止失效
-                            long size = HttpUtil.downloadFile(wxMessage.getPicUrl(), FileUtil.file(filepath));
-                            log.info("download pic size : {}", size);
-                            wxFansMsg.setPicPath(filepath);
-                            wxFansMsg.setIsRes("0");
-                            mpMessageService.createWxFansMsg(wxFansMsg);
-                        }
-
-                    }
-                }
-            } catch (WxErrorException e) {
-                if (e.getError().getErrorCode() == 48001) {
-                    log.info("该公众号没有获取用户信息权限!");
-                }
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-
-        }
-
-        //当用户输入关键词如“你好”,“客服”等,并且有客服在线时,把消息转发给在线客服
-        try {
-            if (StringUtils.startsWithAny(wxMessage.getContent(), "你好", "客服")
-                    && weixinService.getKefuService().kfOnlineList()
-                    .getKfOnlineList().size() > 0) {
-                return WxMpXmlOutMessage.TRANSFER_CUSTOMER_SERVICE()
-                        .fromUser(wxMessage.getToUser())
-                        .toUser(wxMessage.getFromUser()).build();
-            }
-        } catch (WxErrorException e) {
-            e.printStackTrace();
-        }
+//        if (!wxMessage.getMsgType().equals(WxConsts.XmlMsgType.EVENT)) {
+//            //可以选择将消息保存到本地
+//
+//            // 获取微信用户基本信息
+//            try {
+//                WxMpUser wxmpUser = weixinService.getUserService()
+//                        .userInfo(wxMessage.getFromUser(), null);
+//                if (wxmpUser != null) {
+//                    MpAccountDO wxAccount = mpAccountService.findBy(MpAccountDO::getAccount, wxMessage.getToUser());
+//                    if (wxAccount != null) {
+//
+//                        if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.TEXT)) {
+//                            WxFansMsgCreateReqVO wxFansMsg = new WxFansMsgCreateReqVO();
+//                            wxFansMsg.setOpenid(wxmpUser.getOpenId());
+//                            try {
+//                                wxFansMsg.setNickname(wxmpUser.getNickname().getBytes("UTF-8"));
+//                            } catch (UnsupportedEncodingException e) {
+//                                e.printStackTrace();
+//                            }
+//                            wxFansMsg.setHeadimgUrl(wxmpUser.getHeadImgUrl());
+//                            wxFansMsg.setWxAccountId(String.valueOf(wxAccount.getId()));
+//                            wxFansMsg.setMsgType(wxMessage.getMsgType());
+//                            wxFansMsg.setContent(wxMessage.getContent());
+//                            wxFansMsg.setIsRes("1");
+//
+//                            //组装回复消息
+//                            String content = processContent(wxMessage);
+//                            content = HtmlUtil.escape(content);
+//                            wxFansMsg.setResContent(content);
+//
+//                            mpMessageService.createWxFansMsg(wxFansMsg);
+//                            return new TextBuilder().build(content, wxMessage, weixinService);
+//
+//                        }
+//                        if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.IMAGE)) {
+//                            WxFansMsgCreateReqVO wxFansMsg = new WxFansMsgCreateReqVO();
+//                            wxFansMsg.setOpenid(wxmpUser.getOpenId());
+//                            try {
+//                                wxFansMsg.setNickname(wxmpUser.getNickname().getBytes("UTF-8"));
+//                            } catch (UnsupportedEncodingException e) {
+//                                e.printStackTrace();
+//                            }
+//                            wxFansMsg.setHeadimgUrl(wxmpUser.getHeadImgUrl());
+//                            wxFansMsg.setWxAccountId(String.valueOf(wxAccount.getId()));
+//                            wxFansMsg.setMsgType(wxMessage.getMsgType());
+//                            wxFansMsg.setMediaId(wxMessage.getMediaId());
+//                            wxFansMsg.setPicUrl(wxMessage.getPicUrl());
+//                            String downloadDirStr = fileApi.createFile(HttpUtil.downloadBytes(wxMessage.getPicUrl()));
+//                            File downloadDir = new File(downloadDirStr);
+//                            if (!downloadDir.exists()) {
+//                                downloadDir.mkdirs();
+//                            }
+//                            String filepath = downloadDirStr + System.currentTimeMillis() + ".png";
+//                            //微信pic url下载到本地,防止失效
+//                            long size = HttpUtil.downloadFile(wxMessage.getPicUrl(), FileUtil.file(filepath));
+//                            log.info("download pic size : {}", size);
+//                            wxFansMsg.setPicPath(filepath);
+//                            wxFansMsg.setIsRes("0");
+//                            mpMessageService.createWxFansMsg(wxFansMsg);
+//                        }
+//
+//                    }
+//                }
+//            } catch (WxErrorException e) {
+//                if (e.getError().getErrorCode() == 48001) {
+//                    log.info("该公众号没有获取用户信息权限!");
+//                }
+//            } catch (Exception e) {
+//                e.printStackTrace();
+//            }
+//
+//        }
+//
+//        //当用户输入关键词如“你好”,“客服”等,并且有客服在线时,把消息转发给在线客服
+//        try {
+//            if (StringUtils.startsWithAny(wxMessage.getContent(), "你好", "客服")
+//                    && weixinService.getKefuService().kfOnlineList()
+//                    .getKfOnlineList().size() > 0) {
+//                return WxMpXmlOutMessage.TRANSFER_CUSTOMER_SERVICE()
+//                        .fromUser(wxMessage.getToUser())
+//                        .toUser(wxMessage.getFromUser()).build();
+//            }
+//        } catch (WxErrorException e) {
+//            e.printStackTrace();
+//        }
 
         //组装默认回复消息
         return new TextBuilder().build("测试", wxMessage, weixinService);

+ 10 - 4
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageLogHandler.java → yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/handler/message/MessageReceiveHandler.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.mp.service.handler.message;
 
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder;
+import cn.iocoder.yudao.module.mp.service.message.MpMessageService;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.session.WxSessionManager;
 import me.chanjar.weixin.mp.api.WxMpMessageHandler;
@@ -9,21 +10,26 @@ import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 import org.springframework.stereotype.Component;
 
+import javax.annotation.Resource;
 import java.util.Map;
 
 /**
  * 保存微信消息的事件处理器
  *
- * // TODO 芋艿:实现一下
+ * @author 芋道源码
  */
 @Component
 @Slf4j
-public class MessageLogHandler implements WxMpMessageHandler {
+public class MessageReceiveHandler implements WxMpMessageHandler {
+
+    @Resource
+    private MpMessageService mpMessageService;
 
     @Override
     public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context,
                                     WxMpService wxMpService, WxSessionManager sessionManager) {
-        log.info("接收到请求消息,内容:{}", JsonUtils.toJsonString(wxMessage));
+        log.info("[handle][接收到请求消息,内容:{}]", wxMessage);
+        mpMessageService.createFromUser(MpContextHolder.getAppId(), wxMessage);
         return null;
     }
 

+ 9 - 1
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;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 
 /**
  * 粉丝消息表 Service 接口
@@ -12,11 +13,18 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
 public interface MpMessageService {
 
     /**
-     * 获得粉丝消息表 分页
+     * 获得粉丝消息表分页
      *
      * @param pageReqVO 分页查询
      * @return 粉丝消息表 分页
      */
     PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO);
 
+    /**
+     * 保存粉丝消息,来自用户发送
+     *
+     * @param appId 微信公众号 appId
+     * @param wxMessage 消息
+     */
+    void createFromUser(String appId, WxMpXmlMessage wxMessage);
 }

+ 86 - 1
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageServiceImpl.java

@@ -1,6 +1,26 @@
 package cn.iocoder.yudao.module.mp.service.message;
 
+import cn.hutool.core.io.FileTypeUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.io.FileUtils;
+import cn.iocoder.yudao.module.infra.api.file.FileApi;
+import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert;
+import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
+import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum;
+import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
+import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
+import cn.iocoder.yudao.module.mp.service.user.MpUserService;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -10,7 +30,9 @@ import org.springframework.validation.annotation.Validated;
 import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 
-import cn.iocoder.yudao.module.mp.dal.mysql.fansmsg.MpMessageMapper;
+import cn.iocoder.yudao.module.mp.dal.mysql.message.MpMessageMapper;
+
+import java.io.File;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 
@@ -21,14 +43,77 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
  */
 @Service
 @Validated
+@Slf4j
 public class MpMessageServiceImpl implements MpMessageService {
 
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private MpAccountService mpAccountService;
+    @Resource
+    private MpUserService mpUserService;
+
     @Resource
     private MpMessageMapper mpMessageMapper;
 
+    @Resource
+    @Lazy // 延迟加载,解决循环依赖的问题
+    private MpServiceFactory mpServiceFactory;
+
+    @Resource
+    private FileApi fileApi;
+
     @Override
     public PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO) {
         return mpMessageMapper.selectPage(pageReqVO);
     }
 
+    @Override
+    public void createFromUser(String appId, WxMpXmlMessage wxMessage) {
+        WxMpService mpService = mpServiceFactory.getRequiredMpService(appId);
+        // 获得关联信息
+        MpAccountDO account = mpAccountService.getAccountFromCache(appId);
+        Assert.notNull(account, "公众号账号({}) 不存在", appId);
+        MpUserDO user = mpUserService.getUser(appId, wxMessage.getFromUser());
+        Assert.notNull(account, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser());
+
+        // 记录消息
+        MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);
+        message.setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
+        if (StrUtil.isNotEmpty(message.getMediaId())) {
+            message.setMediaUrl(mediaDownload(mpService, message.getMediaId()));
+        }
+        if (StrUtil.isNotEmpty(message.getThumbMediaId())) {
+            message.setThumbMediaUrl(mediaDownload(mpService, message.getThumbMediaId()));
+        }
+        mpMessageMapper.insert(message);
+
+//        WxConsts.MenuButtonType.VIEW
+//        wxMessage.getEventKey()
+
+//        WxConsts.MenuButtonType.CLICK
+//          wxMessage.getEventKey()
+    }
+
+    /**
+     * 下载微信媒体文件的内容,并上传到文件服务
+     *
+     * 为什么要下载?媒体文件在微信后台保存时间为 3 天,即 3 天后 media_id 失效。
+     *
+     * @param mpService 微信公众号 Service
+     * @param mediaId 媒体文件编号
+     * @return 上传后的 URL
+     */
+    private String mediaDownload(WxMpService mpService, String mediaId) {
+        try {
+            // 第一步,从公众号下载媒体文件
+            File file = mpService.getMaterialService().mediaDownload(mediaId);
+            // 第二步,上传到文件服务
+            String path = mediaId + "." + FileTypeUtil.getType(file);
+            return fileApi.createFile(path, FileUtil.readBytes(file));
+        } catch (WxErrorException e) {
+            log.error("[mediaDownload][media({}) 下载失败]", mediaId);
+        }
+        return null;
+    }
+
 }

+ 9 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserService.java

@@ -23,6 +23,15 @@ public interface MpUserService {
      */
     MpUserDO getUser(Long id);
 
+    /**
+     * 使用 appId + openId,获得微信公众号粉丝
+     *
+     * @param appId 微信公众号 appId
+     * @param openId 微信公众号 openId
+     * @return 微信公众号粉丝
+     */
+    MpUserDO getUser(String appId, String openId);
+
     /**
      * 获得微信公众号粉丝列表
      *

+ 6 - 1
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;
 import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert;
 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
-import cn.iocoder.yudao.module.mp.dal.mysql.accountfans.MpUserMapper;
+import cn.iocoder.yudao.module.mp.dal.mysql.user.MpUserMapper;
 import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.mp.bean.result.WxMpUser;
@@ -41,6 +41,11 @@ public class MpUserServiceImpl implements MpUserService {
         return mpUserMapper.selectById(id);
     }
 
+    @Override
+    public MpUserDO getUser(String appId, String openId) {
+        return mpUserMapper.selectByAppIdAndOpenid(appId, openId);
+    }
+
     @Override
     public List<MpUserDO> getUserList(Collection<Long> ids) {
         return mpUserMapper.selectBatchIds(ids);