Browse Source

mp:实现关键字,自动回复

YunaiV 2 năm trước cách đây
mục cha
commit
44f0bcd182

+ 1 - 1
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyMatchEnum.java

@@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.Getter;
 
 
 /**
 /**
- * 微信消息自动回复的匹配模式
+ * 公众号消息自动回复的匹配模式
  *
  *
  * @author 芋道源码
  * @author 芋道源码
  */
  */

+ 2 - 2
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/message/MpAutoReplyTypeEnum.java

@@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.Getter;
 
 
 /**
 /**
- * 微信消息自动回复的类型
+ * 公众号消息自动回复的类型
  *
  *
  * @author 芋道源码
  * @author 芋道源码
  */
  */
@@ -14,7 +14,7 @@ public enum MpAutoReplyTypeEnum {
 
 
     SUBSCRIBE(1, "关注时回复"),
     SUBSCRIBE(1, "关注时回复"),
     MESSAGE(2, "收到消息回复"),
     MESSAGE(2, "收到消息回复"),
-    KEYWORD(2, "关键词回复"),
+    KEYWORD(3, "关键词回复"),
     ;
     ;
 
 
     /**
     /**

+ 73 - 4
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/message/MpMessageConvert.java

@@ -1,18 +1,27 @@
 package cn.iocoder.yudao.module.mp.convert.message;
 package cn.iocoder.yudao.module.mp.convert.message;
 
 
-import java.util.*;
-
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessageRespVO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
 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.dal.dataobject.user.MpUserDO;
+import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage;
+import me.chanjar.weixin.mp.builder.outxml.BaseBuilder;
+import me.chanjar.weixin.mp.builder.outxml.TextBuilder;
+import me.chanjar.weixin.mp.builder.outxml.VideoBuilder;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
 import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 import org.mapstruct.factory.Mappers;
-import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*;
+
+import java.util.Collections;
+import java.util.List;
 
 
 @Mapper
 @Mapper
 public interface MpMessageConvert {
 public interface MpMessageConvert {
@@ -41,4 +50,64 @@ public interface MpMessageConvert {
         }
         }
         return message;
         return message;
     }
     }
+
+    default MpMessageDO convert(MpAutoReplyDO reply, MpAccountDO account, MpUserDO user) {
+        // 构建消息
+        MpMessageDO message = new MpMessageDO();
+        message.setType(reply.getResponseMessageType());
+        // 1. 文本
+        if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.TEXT)) {
+            message.setContent(reply.getResponseContent());
+        } else if (ObjectUtils.equalsAny(reply.getResponseMessageType(), WxConsts.XmlMsgType.IMAGE,  // 2. 图片
+                WxConsts.XmlMsgType.VOICE)) { // 3. 语音
+            message.setMediaId(reply.getResponseMediaId()).setMediaUrl(reply.getResponseMediaUrl());
+        } else if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.VIDEO)) { // 4. 视频
+            message.setMediaId(reply.getResponseMediaId()).setMediaUrl(reply.getResponseMediaUrl())
+                    .setTitle(reply.getResponseTitle()).setDescription(reply.getResponseDescription());
+        } else if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.NEWS)) { // 5. 图文
+            message.setArticles(Collections.singletonList(reply.getResponseArticle()));
+        } else {
+            throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
+        }
+
+        // 其它字段
+        if (account != null) {
+            message.setAccountId(account.getId()).setAppId(account.getAppId());
+        }
+        if (user != null) {
+            message.setUserId(user.getId()).setOpenid(user.getOpenid());
+        }
+        return message;
+    }
+
+    default WxMpXmlOutMessage convert02(MpMessageDO message, MpAccountDO account) {
+        BaseBuilder<?, ? extends WxMpXmlOutMessage> messageBuilder;
+        // 个性化字段
+        switch (message.getType()) {
+            case WxConsts.XmlMsgType.TEXT:
+                messageBuilder = WxMpXmlOutMessage.TEXT().content(message.getContent());
+                break;
+            case WxConsts.XmlMsgType.IMAGE:
+                messageBuilder = WxMpXmlOutMessage.IMAGE().mediaId(message.getMediaId());
+                break;
+            case WxConsts.XmlMsgType.VOICE:
+                messageBuilder = WxMpXmlOutMessage.VOICE().mediaId(message.getMediaId());
+                break;
+            case WxConsts.XmlMsgType.VIDEO:
+                messageBuilder = WxMpXmlOutMessage.VIDEO().mediaId(message.getMediaId())
+                        .title(message.getTitle()).description(message.getDescription());
+                break;
+            case WxConsts.XmlMsgType.NEWS:
+                messageBuilder = WxMpXmlOutMessage.NEWS().articles(convertList02(message.getArticles()));
+                break;
+            default:
+                throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
+        }
+        // 通用字段
+        messageBuilder.fromUser(account.getAccount());
+        messageBuilder.toUser(message.getOpenid());
+        return messageBuilder.build();
+    }
+    List<WxMpXmlOutNewsMessage.Item> convertList02(List<MpMessageDO.Article> list);
+
 }
 }

+ 35 - 1
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/message/MpAutoReplyDO.java

@@ -6,18 +6,21 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyMatchEnum;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyMatchEnum;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
 import lombok.Data;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 import lombok.ToString;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
 import me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
 
 
+import java.util.List;
 import java.util.Set;
 import java.util.Set;
 
 
 /**
 /**
- * 微信消息自动回复 DO
+ * 公众号消息自动回复 DO
  *
  *
  * @author 芋道源码
  * @author 芋道源码
  */
  */
@@ -99,4 +102,35 @@ public class MpAutoReplyDO extends BaseDO {
      */
      */
     private String responseContent;
     private String responseContent;
 
 
+    /**
+     * 回复的媒体 id
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
+     */
+    private String responseMediaId;
+    /**
+     * 回复的媒体 URL
+     */
+    private String responseMediaUrl;
+
+    /**
+     * 回复的标题
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
+     */
+    private String responseTitle;
+    /**
+     * 回复的描述
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO
+     */
+    private String responseDescription;
+
+    /**
+     * 回复的图文消息
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private MpMessageDO.Article responseArticle;
 }
 }

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

@@ -1,17 +1,23 @@
 package cn.iocoder.yudao.module.mp.dal.dataobject.message;
 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.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
 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.dataobject.user.MpUserDO;
 import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum;
 import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
 import lombok.*;
 import lombok.*;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.common.api.WxConsts;
 
 
+import java.io.Serializable;
+import java.util.List;
+
 /**
 /**
- * 微信消息 DO
+ * 公众号消息 DO
  *
  *
  * @author 芋道源码
  * @author 芋道源码
  */
  */
@@ -160,6 +166,14 @@ public class MpMessageDO extends BaseDO {
      */
      */
     private String label;
     private String label;
 
 
+    /**
+     * 图文消息数组
+     *
+     * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS
+     */
+    @TableField(typeHandler = ArticleTypeHandler.class)
+    private List<Article> articles;
+
     // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
     // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
 
 
     /**
     /**
@@ -176,4 +190,45 @@ public class MpMessageDO extends BaseDO {
      */
      */
     private String eventKey;
     private String eventKey;
 
 
+    /**
+     * 文章
+     */
+    @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;
+
+    }
+
+    // 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);
+        }
+
+    }
 }
 }

+ 2 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpAutoReplyMapper.java

@@ -5,9 +5,11 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyMatchEnum;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyMatchEnum;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum;
+import org.apache.ibatis.annotations.Mapper;
 
 
 import java.util.List;
 import java.util.List;
 
 
+@Mapper
 public interface MpAutoReplyMapper extends BaseMapperX<MpAutoReplyDO> {
 public interface MpAutoReplyMapper extends BaseMapperX<MpAutoReplyDO> {
 
 
     default List<MpAutoReplyDO> selectListByAppIdAndKeywordAll(String appId, String requestKeyword) {
     default List<MpAutoReplyDO> selectListByAppIdAndKeywordAll(String appId, String requestKeyword) {

+ 9 - 6
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpAutoReplyServiceImpl.java

@@ -1,12 +1,14 @@
 package cn.iocoder.yudao.module.mp.service.message;
 package cn.iocoder.yudao.module.mp.service.message;
 
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.module.mp.builder.TextBuilder;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
 import cn.iocoder.yudao.module.mp.dal.mysql.message.MpAutoReplyMapper;
 import cn.iocoder.yudao.module.mp.dal.mysql.message.MpAutoReplyMapper;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyMatchEnum;
 import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyMatchEnum;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.validation.annotation.Validated;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
@@ -17,18 +19,21 @@ import java.util.List;
  *
  *
  * @author 芋道源码
  * @author 芋道源码
  */
  */
-@Resource
+@Service
 @Validated
 @Validated
 public class MpAutoReplyServiceImpl implements MpAutoReplyService {
 public class MpAutoReplyServiceImpl implements MpAutoReplyService {
 
 
     @Resource
     @Resource
-    private MpAutoReplyService mpAutoReplyService;
+    private MpMessageService mpMessageService;
 
 
     @Resource
     @Resource
     private MpAutoReplyMapper mpAutoReplyMapper;
     private MpAutoReplyMapper mpAutoReplyMapper;
 
 
     @Override
     @Override
     public WxMpXmlOutMessage replyForMessage(String appId, WxMpXmlMessage wxMessage) {
     public WxMpXmlOutMessage replyForMessage(String appId, WxMpXmlMessage wxMessage) {
+//        if (true) {
+//            return new TextBuilder().build("nihao", wxMessage, null);
+//        }
         // 第一步,匹配自动回复
         // 第一步,匹配自动回复
         List<MpAutoReplyDO> replies = null;
         List<MpAutoReplyDO> replies = null;
         // 1.1 关键字
         // 1.1 关键字
@@ -47,12 +52,10 @@ public class MpAutoReplyServiceImpl implements MpAutoReplyService {
         if (CollUtil.isEmpty(replies)) {
         if (CollUtil.isEmpty(replies)) {
             return null;
             return null;
         }
         }
+        MpAutoReplyDO reply = CollUtil.getFirst(replies);
 
 
         // 第二步,基于自动回复,创建消息
         // 第二步,基于自动回复,创建消息
-
-
-        // 第三步,将消息转换成 WxMpXmlOutMessage 返回
-        return null;
+        return mpMessageService.createFromAutoReply(wxMessage.getFromUser(), reply);
     }
     }
 
 
 }
 }

+ 12 - 0
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/message/MpMessageService.java

@@ -2,8 +2,10 @@ package cn.iocoder.yudao.module.mp.service.message;
 
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.controller.admin.message.vo.MpMessagePageReqVO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 
 
 /**
 /**
  * 粉丝消息表 Service 接口
  * 粉丝消息表 Service 接口
@@ -28,4 +30,14 @@ public interface MpMessageService {
      * @param wxMessage 消息
      * @param wxMessage 消息
      */
      */
     void createFromUser(String appId, WxMpXmlMessage wxMessage);
     void createFromUser(String appId, WxMpXmlMessage wxMessage);
+
+    /**
+     * 创建粉丝消息,通过自动回复
+     *
+     * @param openid 公众号粉丝 openid
+     * @param reply 自动回复
+     * @return 微信回复消息 XML
+     */
+    WxMpXmlOutMessage createFromAutoReply(String openid, MpAutoReplyDO reply);
+
 }
 }

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

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.io.FileUtils;
 import cn.iocoder.yudao.module.infra.api.file.FileApi;
 import cn.iocoder.yudao.module.infra.api.file.FileApi;
 import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert;
 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.account.MpAccountDO;
+import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
 import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
 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.dal.dataobject.user.MpUserDO;
 import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum;
 import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum;
@@ -20,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
 import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
@@ -74,7 +76,7 @@ public class MpMessageServiceImpl implements MpMessageService {
         MpAccountDO account = mpAccountService.getAccountFromCache(appId);
         MpAccountDO account = mpAccountService.getAccountFromCache(appId);
         Assert.notNull(account, "公众号账号({}) 不存在", appId);
         Assert.notNull(account, "公众号账号({}) 不存在", appId);
         MpUserDO user = mpUserService.getUser(appId, wxMessage.getFromUser());
         MpUserDO user = mpUserService.getUser(appId, wxMessage.getFromUser());
-        Assert.notNull(account, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser());
+        Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser());
 
 
         // 记录消息
         // 记录消息
         MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);
         MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user);
@@ -94,6 +96,23 @@ public class MpMessageServiceImpl implements MpMessageService {
 //          wxMessage.getEventKey()
 //          wxMessage.getEventKey()
     }
     }
 
 
+    @Override
+    public WxMpXmlOutMessage createFromAutoReply(String openid, MpAutoReplyDO reply) {
+        // 获得关联信息
+        MpAccountDO account = mpAccountService.getAccountFromCache(reply.getAppId());
+        Assert.notNull(account, "公众号账号({}) 不存在", reply.getAppId());
+        MpUserDO user = mpUserService.getUser(reply.getAppId(), openid);
+        Assert.notNull(user, "公众号粉丝({}/{}) 不存在", reply.getAppId(), openid);
+
+        // 记录消息
+        MpMessageDO message = MpMessageConvert.INSTANCE.convert(reply, account, user);
+        message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
+        mpMessageMapper.insert(message);
+
+        // 转换返回 WxMpXmlOutMessage 对象
+        return MpMessageConvert.INSTANCE.convert02(message, account);
+    }
+
     /**
     /**
      * 下载微信媒体文件的内容,并上传到文件服务
      * 下载微信媒体文件的内容,并上传到文件服务
      *
      *