mp:重构 message 发送消息的方法

This commit is contained in:
YunaiV 2023-01-06 09:25:19 +08:00
parent 47f071560f
commit 665f4b2b09
15 changed files with 453 additions and 84 deletions

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mp.convert.message;
import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpAutoReplyDO;
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
@Mapper
public interface MpAutoReplyConvert {
MpAutoReplyConvert INSTANCE = Mappers.getMapper(MpAutoReplyConvert.class);
@Mappings({
@Mapping(source = "reply.appId", target = "appId"),
@Mapping(source = "reply.responseMessageType", target = "type"),
@Mapping(source = "reply.responseContent", target = "content"),
@Mapping(source = "reply.responseMediaId", target = "mediaId"),
@Mapping(source = "reply.responseMediaUrl", target = "mediaUrl"),
@Mapping(source = "reply.responseTitle", target = "title"),
@Mapping(source = "reply.responseDescription", target = "description"),
@Mapping(source = "reply.responseArticle", target = "article"),
})
MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply);
}

View File

@ -1,20 +1,16 @@
package cn.iocoder.yudao.module.mp.convert.message; package cn.iocoder.yudao.module.mp.convert.message;
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.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 cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
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 me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage; import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage;
import me.chanjar.weixin.mp.builder.outxml.BaseBuilder; 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;
@ -51,22 +47,26 @@ public interface MpMessageConvert {
return message; return message;
} }
default MpMessageDO convert(MpAutoReplyDO reply, MpAccountDO account, MpUserDO user) { default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, MpAccountDO account, MpUserDO user) {
// 构建消息 // 构建消息
MpMessageDO message = new MpMessageDO(); MpMessageDO message = new MpMessageDO();
message.setType(reply.getResponseMessageType()); message.setType(sendReqBO.getType());
// 1. 文本 switch (sendReqBO.getType()) {
if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.TEXT)) { case WxConsts.XmlMsgType.TEXT: // 1. 文本
message.setContent(reply.getResponseContent()); message.setContent(sendReqBO.getContent());
} else if (ObjectUtils.equalsAny(reply.getResponseMessageType(), WxConsts.XmlMsgType.IMAGE, // 2. 图片 break;
WxConsts.XmlMsgType.VOICE)) { // 3. 语音 case WxConsts.XmlMsgType.IMAGE: // 2. 图片
message.setMediaId(reply.getResponseMediaId()).setMediaUrl(reply.getResponseMediaUrl()); case WxConsts.XmlMsgType.VOICE: // 3. 语音
} else if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.VIDEO)) { // 4. 视频 message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl());
message.setMediaId(reply.getResponseMediaId()).setMediaUrl(reply.getResponseMediaUrl()) break;
.setTitle(reply.getResponseTitle()).setDescription(reply.getResponseDescription()); case WxConsts.XmlMsgType.VIDEO: // 4. 视频
} else if (StrUtil.equals(reply.getResponseMessageType(), WxConsts.XmlMsgType.NEWS)) { // 5. 图文 message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl())
message.setArticles(Collections.singletonList(reply.getResponseArticle())); .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription());
} else { break;
case WxConsts.XmlMsgType.NEWS: // 5. 图文
message.setArticles(Collections.singletonList(sendReqBO.getArticle()));
break;
default:
throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
} }

View File

@ -1,15 +1,19 @@
package cn.iocoder.yudao.module.mp.dal.dataobject.menu; package cn.iocoder.yudao.module.mp.dal.dataobject.menu;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 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.account.MpAccountDO;
import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO; import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
import lombok.*; import lombok.Data;
import lombok.EqualsAndHashCode;
import com.baomidou.mybatisplus.annotation.*; import lombok.ToString;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.bean.menu.WxMenu; import me.chanjar.weixin.common.api.WxConsts.MenuButtonType;
import me.chanjar.weixin.common.bean.menu.WxMenuButton;
import java.util.List; import java.util.List;
@ -48,8 +52,8 @@ public class MpMenuDO extends BaseDO {
/** /**
* 按钮列表 * 按钮列表
*/ */
@TableField(typeHandler = WxMenuButtonTypeHandler.class) @TableField(typeHandler = ButtonTypeHandler.class)
private List<WxMenuButton> buttons; private List<Button> buttons;
/** /**
* 同步状态 * 同步状态
* *
@ -58,16 +62,113 @@ public class MpMenuDO extends BaseDO {
*/ */
private Boolean syncStatus; private Boolean syncStatus;
/**
* 按钮
*/
@Data
public static class Button {
/**
* 类型
*
* 枚举 {@link MenuButtonType}
*/
private String type;
/**
* 消息类型
*
* {@link #type} CLICKSCANCODE_WAITMSG
*
* 枚举 {@link WxConsts.XmlMsgType} 中的 TEXTIMAGEVOICEVIDEONEWS
*/
private String messageType;
/**
* 名称
*/
private String name;
/**
* 标识
*/
private String key;
/**
* 二级菜单列表
*/
private List<Button> subButtons;
/**
* 网页链接
*
* 用户点击菜单可打开链接不超过 1024 字节
*
* 类型为 {@link WxConsts.XmlMsgType} VIEWMINIPROGRAM
*/
private String url;
/**
* 小程序的 appId
*
* 类型为 {@link WxConsts.XmlMsgType} MINIPROGRAM
*/
private String appId;
/**
* 小程序的页面路径
*
* 类型为 {@link WxConsts.XmlMsgType} MINIPROGRAM
*/
private String pagePath;
/**
* 消息内容
*
* 消息类型为 {@link WxConsts.XmlMsgType} TEXT
*/
private String content;
/**
* 媒体 id
*
* 消息类型为 {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
private String mediaId;
/**
* 媒体 URL
*
* 消息类型为 {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
private String mediaUrl;
/**
* 回复的标题
*
* 消息类型为 {@link WxConsts.XmlMsgType} VIDEO
*/
private String title;
/**
* 回复的描述
*
* 消息类型为 {@link WxConsts.XmlMsgType} VIDEO
*/
private String description;
/**
* 图文消息
*
* 消息类型为 {@link WxConsts.XmlMsgType} NEWS
*/
private MpMessageDO.Article article;
}
// TODO @芋艿可以找一些新的思路 // TODO @芋艿可以找一些新的思路
public static class WxMenuButtonTypeHandler extends AbstractJsonTypeHandler<List<WxMenuButton>> { public static class ButtonTypeHandler extends AbstractJsonTypeHandler<List<Button>> {
@Override @Override
protected List<WxMenuButton> parse(String json) { protected List<Button> parse(String json) {
return JsonUtils.parseArray(json, WxMenuButton.class); return JsonUtils.parseArray(json, Button.class);
} }
@Override @Override
protected String toJson(List<WxMenuButton> obj) { protected String toJson(List<Button> obj) {
return JsonUtils.toJsonString(obj); return JsonUtils.toJsonString(obj);
} }

View File

@ -16,7 +16,6 @@ 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;
/** /**

View File

@ -12,7 +12,9 @@ import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; 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 me.chanjar.weixin.mp.builder.kefu.NewsBuilder;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
@ -199,20 +201,24 @@ public class MpMessageDO extends BaseDO {
/** /**
* 图文消息标题 * 图文消息标题
*/ */
@NotEmpty(message = "图文消息标题不能为空", groups = NewsBuilder.class)
private String title; private String title;
/** /**
* 图文消息描述 * 图文消息描述
*/ */
@NotEmpty(message = "图文消息描述不能为空", groups = NewsBuilder.class)
private String description; private String description;
/** /**
* 图片链接 * 图片链接
* *
* 支持JPGPNG格式较好的效果为大图 360*200小图 200*200 * 支持 JPGPNG 格式较好的效果为大图 360*200小图 200*200
*/ */
@NotEmpty(message = "图片链接不能为空", groups = NewsBuilder.class)
private String picUrl; private String picUrl;
/** /**
* 点击图文消息跳转链接 * 点击图文消息跳转链接
*/ */
@NotEmpty(message = "点击图文消息跳转链接不能为空", groups = NewsBuilder.class)
private String url; private String url;
} }

View File

@ -7,4 +7,8 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper @Mapper
public interface MpMenuMapper extends BaseMapperX<MpMenuDO> { public interface MpMenuMapper extends BaseMapperX<MpMenuDO> {
default MpMenuDO selectByAppId(String appId) {
return selectOne(MpMenuDO::getAppId, appId);
}
} }

View File

@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.mp.framework.mp.core.util;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import javax.validation.Validator;
/**
* 公众号工具类
*
* @author 芋道源码
*/
@Slf4j
public class MpUtils {
/**
* Text 类型的消息参数校验 Group
*/
public interface TextGroup {}
/**
* Image 类型的消息参数校验 Group
*/
public interface ImageGroup {}
/**
* Voice 类型的消息参数校验 Group
*/
public interface VoiceGroup {}
/**
* Video 类型的消息参数校验 Group
*/
public interface VideoGroup {}
/**
* News 类型的消息参数校验 Group
*/
public interface NewsGroup {}
/**
* 校验消息的格式是否符合要求
*
* @param type 类型
* @param message 消息
*/
public static void validateMessage(Validator validator, String type, Object message) {
// 获得对应的校验 group
Class<?> group;
switch (type) {
case WxConsts.XmlMsgType.TEXT:
group = TextGroup.class;
break;
case WxConsts.XmlMsgType.IMAGE:
group = ImageGroup.class;
break;
case WxConsts.XmlMsgType.VOICE:
group = VoiceGroup.class;
break;
case WxConsts.XmlMsgType.VIDEO:
group = VideoGroup.class;
break;
case WxConsts.XmlMsgType.NEWS:
group = NewsGroup.class;
break;
default:
log.error("[validateMessage][未知的消息类型({})]", message);
throw new IllegalArgumentException("不支持的消息类型:" + type);
}
// 执行校验
ValidationUtils.validate(validator, message, group);
}
}

View File

@ -1,12 +1,16 @@
package cn.iocoder.yudao.module.mp.service.handler.menu; package cn.iocoder.yudao.module.mp.service.handler.menu;
import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder;
import cn.iocoder.yudao.module.mp.service.menu.MpMenuService;
import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpMenuService;
import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpMessageHandler;
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 me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Map; import java.util.Map;
import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType; import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType;
@ -14,24 +18,20 @@ import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType;
/** /**
* 自定义菜单的事件处理器 * 自定义菜单的事件处理器
* *
* // TODO 芋艿待实现 * 逻辑用户点击菜单时触发对应的回复
*
* @author 芋道源码
*/ */
@Component @Component
public class MenuHandler implements WxMpMessageHandler { public class MenuHandler implements WxMpMessageHandler {
@Resource
private MpMenuService mpMenuService;
@Override @Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context,
Map<String, Object> context, WxMpService weixinService, WxMpService weixinService, WxSessionManager sessionManager) {
WxSessionManager sessionManager) { return mpMenuService.reply(MpContextHolder.getAppId(), wxMessage.getEventKey(), wxMessage.getFromUser());
String msg = wxMessage.getEventKey();
if (MenuButtonType.VIEW.equals(wxMessage.getEvent())) {
return null;
}
return WxMpXmlOutMessage.TEXT().content(msg)
.fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.build();
} }
} }

View File

@ -29,7 +29,7 @@ public class MessageReceiveHandler implements WxMpMessageHandler {
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context,
WxMpService wxMpService, WxSessionManager sessionManager) { WxMpService wxMpService, WxSessionManager sessionManager) {
log.info("[handle][接收到请求消息,内容:{}]", wxMessage); log.info("[handle][接收到请求消息,内容:{}]", wxMessage);
mpMessageService.createFromUser(MpContextHolder.getAppId(), wxMessage); mpMessageService.receiveMessage(MpContextHolder.getAppId(), wxMessage);
return null; return null;
} }

View File

@ -4,6 +4,8 @@ import javax.validation.*;
import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.*; import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.*;
import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
/** /**
* 微信菜单 Service 接口 * 微信菜单 Service 接口
@ -35,4 +37,14 @@ public interface MpMenuService {
*/ */
MpMenuDO getMenu(Long id); MpMenuDO getMenu(Long id);
/**
* 用户点击菜单按钮时回复对应的消息
*
* @param appId 公众号 AppId
* @param key 菜单按钮的标识
* @param openid 用户的 openid
* @return 消息
*/
WxMpXmlOutMessage reply(String appId, String key, String openid);
} }

View File

@ -1,11 +1,18 @@
package cn.iocoder.yudao.module.mp.service.menu; package cn.iocoder.yudao.module.mp.service.menu;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mp.convert.menu.MpMenuConvert; import cn.iocoder.yudao.module.mp.convert.menu.MpMenuConvert;
import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO; import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.menu.WxMenu; import me.chanjar.weixin.common.bean.menu.WxMenu;
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.WxMpXmlOutMessage;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -27,6 +34,7 @@ import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*;
*/ */
@Service @Service
@Validated @Validated
@Slf4j
public class MpMenuServiceImpl implements MpMenuService { public class MpMenuServiceImpl implements MpMenuService {
@Resource @Resource
@ -77,4 +85,49 @@ public class MpMenuServiceImpl implements MpMenuService {
return mpMenuMapper.selectById(id); return mpMenuMapper.selectById(id);
} }
@Override
public WxMpXmlOutMessage reply(String appId, String key, String openid) {
// 获得菜单
MpMenuDO menu = mpMenuMapper.selectByAppId(appId);
if (menu == null) {
log.error("[reply][appId({}) 找不到对应的菜单]", appId);
return null;
}
// 匹配对应的按钮
MpMenuDO.Button button = getMenuButton(menu, key);
if (button == null) {
log.error("[reply][appId({}) key({}) 找不到对应的菜单按钮]", appId, key);
return null;
}
// 按钮必须要有消息类型不然后续无法回复消息
if (StrUtil.isEmpty(button.getMessageType())) {
log.error("[reply][appId({}) key({}) 不存在消息类型({})]", appId, key, button);
return null;
}
// 回复消息
return null;
}
private MpMenuDO.Button getMenuButton(MpMenuDO menu, String key) {
// 先查询子按钮
for (MpMenuDO.Button button : menu.getButtons()) {
if (CollUtil.isEmpty(button.getSubButtons())) {
continue;
}
for (MpMenuDO.Button subButton : button.getSubButtons()) {
if (StrUtil.equals(subButton.getKey(), key)) {
return subButton;
}
}
}
// 再查询父按钮
for (MpMenuDO.Button button : menu.getButtons()) {
if (StrUtil.equals(button.getKey(), key)) {
return button;
}
}
return null;
}
} }

View File

@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.mp.service.message;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.mp.convert.message.MpAutoReplyConvert;
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.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.MpAutoReplyTypeEnum; import cn.iocoder.yudao.module.mp.enums.message.MpAutoReplyTypeEnum;
import cn.iocoder.yudao.module.mp.service.account.MpAccountService; import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
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;
@ -58,7 +60,8 @@ public class MpAutoReplyServiceImpl implements MpAutoReplyService {
MpAutoReplyDO reply = CollUtil.getFirst(replies); MpAutoReplyDO reply = CollUtil.getFirst(replies);
// 第二步基于自动回复创建消息 // 第二步基于自动回复创建消息
return mpMessageService.createFromAutoReply(wxMessage.getFromUser(), reply); MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply);
return mpMessageService.sendOutMessage(sendReqBO);
} }
@Override @Override
@ -69,7 +72,8 @@ public class MpAutoReplyServiceImpl implements MpAutoReplyService {
: buildDefaultSubscribeAutoReply(appId); // 如果不存在提供一个默认末班 : buildDefaultSubscribeAutoReply(appId); // 如果不存在提供一个默认末班
// 第二步基于自动回复创建消息 // 第二步基于自动回复创建消息
return mpMessageService.createFromAutoReply(wxMessage.getFromUser(), reply); MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply);
return mpMessageService.sendOutMessage(sendReqBO);
} }
private MpAutoReplyDO buildDefaultSubscribeAutoReply(String appId) { private MpAutoReplyDO buildDefaultSubscribeAutoReply(String appId) {

View File

@ -2,11 +2,13 @@ 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 cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
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 javax.validation.Valid;
/** /**
* 粉丝消息表 Service 接口 * 粉丝消息表 Service 接口
* *
@ -24,20 +26,23 @@ public interface MpMessageService {
PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO); PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO);
/** /**
* 保存粉丝消息来自用户发送 * 从公众号接收到用户消息
* *
* @param appId 微信公众号 appId * @param appId 微信公众号 appId
* @param wxMessage 消息 * @param wxMessage 消息
*/ */
void createFromUser(String appId, WxMpXmlMessage wxMessage); void receiveMessage(String appId, WxMpXmlMessage wxMessage);
/** /**
* 创建粉丝消息通过自动回复 * 使用公众号给用户回复消息
* *
* @param openid 公众号粉丝 openid * 例如说自动回复客服消息菜单回复消息等场景
* @param reply 自动回复 *
* 注意该方法只是返回 WxMpXmlOutMessage 对象不会真的发送消息
*
* @param sendReqBO 消息内容
* @return 微信回复消息 XML * @return 微信回复消息 XML
*/ */
WxMpXmlOutMessage createFromAutoReply(String openid, MpAutoReplyDO reply); WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO);
} }

View File

@ -2,21 +2,22 @@ package cn.iocoder.yudao.module.mp.service.message;
import cn.hutool.core.io.FileTypeUtil; import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.io.FileUtils; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.infra.api.file.FileApi; import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.mp.controller.admin.message.vo.MpMessagePageReqVO;
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.dal.mysql.message.MpMessageMapper;
import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum; 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.framework.mp.core.MpServiceFactory;
import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils;
import cn.iocoder.yudao.module.mp.service.account.MpAccountService; import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
import cn.iocoder.yudao.module.mp.service.user.MpUserService; import cn.iocoder.yudao.module.mp.service.user.MpUserService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; 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;
@ -24,20 +25,12 @@ 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.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import cn.iocoder.yudao.module.mp.controller.admin.message.vo.*; import javax.annotation.Resource;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import javax.validation.Validator;
import cn.iocoder.yudao.module.mp.dal.mysql.message.MpMessageMapper;
import java.io.File; import java.io.File;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/** /**
* 粉丝消息表 Service 实现类 * 粉丝消息表 Service 实现类
* *
@ -64,13 +57,16 @@ public class MpMessageServiceImpl implements MpMessageService {
@Resource @Resource
private FileApi fileApi; private FileApi fileApi;
@Resource
private Validator validator;
@Override @Override
public PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO) { public PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO) {
return mpMessageMapper.selectPage(pageReqVO); return mpMessageMapper.selectPage(pageReqVO);
} }
@Override @Override
public void createFromUser(String appId, WxMpXmlMessage wxMessage) { public void receiveMessage(String appId, WxMpXmlMessage wxMessage) {
WxMpService mpService = mpServiceFactory.getRequiredMpService(appId); WxMpService mpService = mpServiceFactory.getRequiredMpService(appId);
// 获得关联信息 // 获得关联信息
MpAccountDO account = mpAccountService.getAccountFromCache(appId); MpAccountDO account = mpAccountService.getAccountFromCache(appId);
@ -88,24 +84,21 @@ public class MpMessageServiceImpl implements MpMessageService {
message.setThumbMediaUrl(mediaDownload(mpService, message.getThumbMediaId())); message.setThumbMediaUrl(mediaDownload(mpService, message.getThumbMediaId()));
} }
mpMessageMapper.insert(message); mpMessageMapper.insert(message);
// WxConsts.MenuButtonType.VIEW TODO 芋艿待测试
// wxMessage.getEventKey()
// WxConsts.MenuButtonType.CLICK
// wxMessage.getEventKey()
} }
@Override @Override
public WxMpXmlOutMessage createFromAutoReply(String openid, MpAutoReplyDO reply) { public WxMpXmlOutMessage sendOutMessage(MpMessageSendOutReqBO sendReqBO) {
// 校验消息格式
MpUtils.validateMessage(validator, sendReqBO.getType(), sendReqBO);
// 获得关联信息 // 获得关联信息
MpAccountDO account = mpAccountService.getAccountFromCache(reply.getAppId()); MpAccountDO account = mpAccountService.getAccountFromCache(sendReqBO.getAppId());
Assert.notNull(account, "公众号账号({}) 不存在", reply.getAppId()); Assert.notNull(account, "公众号账号({}) 不存在", sendReqBO.getAppId());
MpUserDO user = mpUserService.getUser(reply.getAppId(), openid); MpUserDO user = mpUserService.getUser(sendReqBO.getAppId(), sendReqBO.getOpenid());
Assert.notNull(user, "公众号粉丝({}/{}) 不存在", reply.getAppId(), openid); Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid());
// 记录消息 // 记录消息
MpMessageDO message = MpMessageConvert.INSTANCE.convert(reply, account, user); MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user);
message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); message.setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
mpMessageMapper.insert(message); mpMessageMapper.insert(message);

View File

@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.mp.service.message.bo;
import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils;
import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils.*;
import lombok.Data;
import me.chanjar.weixin.common.api.WxConsts;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 公众号消息发送 Request BO
*
* 为什么要有该 BO 在自动回复客服消息菜单回复消息等场景都涉及到 MP 给用户发送消息所以使用该 BO 统一承接
*
* @author 芋道源码
*/
@Data
public class MpMessageSendOutReqBO {
/**
* 公众号 appId
*/
@NotEmpty(message = "公众号 appId 不能为空")
private String appId;
/**
* 公众号用户 openid
*/
@NotEmpty(message = "公众号用户 openid 不能为空")
private String openid;
// ========== 消息内容 ==========
/**
* 消息类型
*
* 枚举 {@link WxConsts.XmlMsgType} 中的 TEXTIMAGEVOICEVIDEONEWS
*/
@NotEmpty(message = "消息类型不能为空")
public String type;
/**
* 消息内容
*
* 消息类型为 {@link WxConsts.XmlMsgType} TEXT
*/
@NotEmpty(message = "消息内容不能为空", groups = TextGroup.class)
private String content;
/**
* 媒体 id
*
* 消息类型为 {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
@NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
private String mediaId;
/**
* 媒体 URL
*
* 消息类型为 {@link WxConsts.XmlMsgType} IMAGEVOICEVIDEO
*/
@NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
private String mediaUrl;
/**
* 标题
*
* 消息类型为 {@link WxConsts.XmlMsgType} VIDEO
*/
@NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class)
private String title;
/**
* 描述
*
* 消息类型为 {@link WxConsts.XmlMsgType} VIDEO
*/
@NotEmpty(message = "消息内容不能为空", groups = VideoGroup.class)
private String description;
/**
* 图文消息
*
* 消息类型为 {@link WxConsts.XmlMsgType} NEWS
*/
@Valid
@NotNull(message = "图文消息不能为空", groups = NewsGroup.class)
private MpMessageDO.Article article;
}