mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2024-11-25 08:41:52 +08:00
mp:重构 message 发送消息的方法
This commit is contained in:
parent
47f071560f
commit
665f4b2b09
@ -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);
|
||||
|
||||
}
|
@ -1,20 +1,16 @@
|
||||
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.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.message.MpAutoReplyDO;
|
||||
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.service.message.bo.MpMessageSendOutReqBO;
|
||||
import me.chanjar.weixin.common.api.WxConsts;
|
||||
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.Mapping;
|
||||
import org.mapstruct.Mappings;
|
||||
@ -51,23 +47,27 @@ public interface MpMessageConvert {
|
||||
return message;
|
||||
}
|
||||
|
||||
default MpMessageDO convert(MpAutoReplyDO reply, MpAccountDO account, MpUserDO user) {
|
||||
default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, 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());
|
||||
message.setType(sendReqBO.getType());
|
||||
switch (sendReqBO.getType()) {
|
||||
case WxConsts.XmlMsgType.TEXT: // 1. 文本
|
||||
message.setContent(sendReqBO.getContent());
|
||||
break;
|
||||
case WxConsts.XmlMsgType.IMAGE: // 2. 图片
|
||||
case WxConsts.XmlMsgType.VOICE: // 3. 语音
|
||||
message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl());
|
||||
break;
|
||||
case WxConsts.XmlMsgType.VIDEO: // 4. 视频
|
||||
message.setMediaId(sendReqBO.getMediaId()).setMediaUrl(sendReqBO.getMediaUrl())
|
||||
.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription());
|
||||
break;
|
||||
case WxConsts.XmlMsgType.NEWS: // 5. 图文
|
||||
message.setArticles(Collections.singletonList(sendReqBO.getArticle()));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的消息类型:" + message.getType());
|
||||
}
|
||||
|
||||
// 其它字段
|
||||
|
@ -1,15 +1,19 @@
|
||||
package cn.iocoder.yudao.module.mp.dal.dataobject.menu;
|
||||
|
||||
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.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 lombok.*;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import me.chanjar.weixin.common.bean.menu.WxMenu;
|
||||
import me.chanjar.weixin.common.bean.menu.WxMenuButton;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import me.chanjar.weixin.common.api.WxConsts;
|
||||
import me.chanjar.weixin.common.api.WxConsts.MenuButtonType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -48,8 +52,8 @@ public class MpMenuDO extends BaseDO {
|
||||
/**
|
||||
* 按钮列表
|
||||
*/
|
||||
@TableField(typeHandler = WxMenuButtonTypeHandler.class)
|
||||
private List<WxMenuButton> buttons;
|
||||
@TableField(typeHandler = ButtonTypeHandler.class)
|
||||
private List<Button> buttons;
|
||||
/**
|
||||
* 同步状态
|
||||
*
|
||||
@ -58,16 +62,113 @@ public class MpMenuDO extends BaseDO {
|
||||
*/
|
||||
private Boolean syncStatus;
|
||||
|
||||
/**
|
||||
* 按钮
|
||||
*/
|
||||
@Data
|
||||
public static class Button {
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*
|
||||
* 枚举 {@link MenuButtonType}
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* 消息类型
|
||||
*
|
||||
* 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG
|
||||
*
|
||||
* 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS
|
||||
*/
|
||||
private String messageType;
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 标识
|
||||
*/
|
||||
private String key;
|
||||
/**
|
||||
* 二级菜单列表
|
||||
*/
|
||||
private List<Button> subButtons;
|
||||
/**
|
||||
* 网页链接
|
||||
*
|
||||
* 用户点击菜单可打开链接,不超过 1024 字节
|
||||
*
|
||||
* 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM
|
||||
*/
|
||||
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} 的 IMAGE、VOICE、VIDEO
|
||||
*/
|
||||
private String mediaId;
|
||||
/**
|
||||
* 媒体 URL
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
|
||||
*/
|
||||
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 @芋艿:可以找一些新的思路
|
||||
public static class WxMenuButtonTypeHandler extends AbstractJsonTypeHandler<List<WxMenuButton>> {
|
||||
public static class ButtonTypeHandler extends AbstractJsonTypeHandler<List<Button>> {
|
||||
|
||||
@Override
|
||||
protected List<WxMenuButton> parse(String json) {
|
||||
return JsonUtils.parseArray(json, WxMenuButton.class);
|
||||
protected List<Button> parse(String json) {
|
||||
return JsonUtils.parseArray(json, Button.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toJson(List<WxMenuButton> obj) {
|
||||
protected String toJson(List<Button> obj) {
|
||||
return JsonUtils.toJsonString(obj);
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ import lombok.ToString;
|
||||
import me.chanjar.weixin.common.api.WxConsts;
|
||||
import me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
@ -12,7 +12,9 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
|
||||
import lombok.*;
|
||||
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.util.List;
|
||||
|
||||
@ -199,20 +201,24 @@ public class MpMessageDO extends BaseDO {
|
||||
/**
|
||||
* 图文消息标题
|
||||
*/
|
||||
@NotEmpty(message = "图文消息标题不能为空", groups = NewsBuilder.class)
|
||||
private String title;
|
||||
/**
|
||||
* 图文消息描述
|
||||
*/
|
||||
@NotEmpty(message = "图文消息描述不能为空", groups = NewsBuilder.class)
|
||||
private String description;
|
||||
/**
|
||||
* 图片链接
|
||||
*
|
||||
* 支持JPG、PNG格式,较好的效果为大图 360*200,小图 200*200
|
||||
* 支持 JPG、PNG 格式,较好的效果为大图 360*200,小图 200*200
|
||||
*/
|
||||
@NotEmpty(message = "图片链接不能为空", groups = NewsBuilder.class)
|
||||
private String picUrl;
|
||||
/**
|
||||
* 点击图文消息跳转链接
|
||||
*/
|
||||
@NotEmpty(message = "点击图文消息跳转链接不能为空", groups = NewsBuilder.class)
|
||||
private String url;
|
||||
|
||||
}
|
||||
|
@ -7,4 +7,8 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper
|
||||
public interface MpMenuMapper extends BaseMapperX<MpMenuDO> {
|
||||
|
||||
default MpMenuDO selectByAppId(String appId) {
|
||||
return selectOne(MpMenuDO::getAppId, appId);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,16 @@
|
||||
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.mp.api.WxMpMenuService;
|
||||
import me.chanjar.weixin.mp.api.WxMpMessageHandler;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
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;
|
||||
|
||||
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
|
||||
public class MenuHandler implements WxMpMessageHandler {
|
||||
|
||||
@Resource
|
||||
private MpMenuService mpMenuService;
|
||||
|
||||
@Override
|
||||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
|
||||
Map<String, Object> context, WxMpService weixinService,
|
||||
WxSessionManager sessionManager) {
|
||||
|
||||
String msg = wxMessage.getEventKey();
|
||||
if (MenuButtonType.VIEW.equals(wxMessage.getEvent())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return WxMpXmlOutMessage.TEXT().content(msg)
|
||||
.fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
|
||||
.build();
|
||||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context,
|
||||
WxMpService weixinService, WxSessionManager sessionManager) {
|
||||
return mpMenuService.reply(MpContextHolder.getAppId(), wxMessage.getEventKey(), wxMessage.getFromUser());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public class MessageReceiveHandler implements WxMpMessageHandler {
|
||||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context,
|
||||
WxMpService wxMpService, WxSessionManager sessionManager) {
|
||||
log.info("[handle][接收到请求消息,内容:{}]", wxMessage);
|
||||
mpMessageService.createFromUser(MpContextHolder.getAppId(), wxMessage);
|
||||
mpMessageService.receiveMessage(MpContextHolder.getAppId(), wxMessage);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import javax.validation.*;
|
||||
|
||||
import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.*;
|
||||
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 接口
|
||||
@ -35,4 +37,14 @@ public interface MpMenuService {
|
||||
*/
|
||||
MpMenuDO getMenu(Long id);
|
||||
|
||||
/**
|
||||
* 用户点击菜单按钮时,回复对应的消息
|
||||
*
|
||||
* @param appId 公众号 AppId
|
||||
* @param key 菜单按钮的标识
|
||||
* @param openid 用户的 openid
|
||||
* @return 消息
|
||||
*/
|
||||
WxMpXmlOutMessage reply(String appId, String key, String openid);
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
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.dal.dataobject.account.MpAccountDO;
|
||||
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.service.account.MpAccountService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.bean.menu.WxMenu;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -27,6 +34,7 @@ import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*;
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class MpMenuServiceImpl implements MpMenuService {
|
||||
|
||||
@Resource
|
||||
@ -77,4 +85,49 @@ public class MpMenuServiceImpl implements MpMenuService {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,11 +2,13 @@ package cn.iocoder.yudao.module.mp.service.message;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
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.message.MpAutoReplyDO;
|
||||
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.service.account.MpAccountService;
|
||||
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
|
||||
import me.chanjar.weixin.common.api.WxConsts;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
@ -58,7 +60,8 @@ public class MpAutoReplyServiceImpl implements MpAutoReplyService {
|
||||
MpAutoReplyDO reply = CollUtil.getFirst(replies);
|
||||
|
||||
// 第二步,基于自动回复,创建消息
|
||||
return mpMessageService.createFromAutoReply(wxMessage.getFromUser(), reply);
|
||||
MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply);
|
||||
return mpMessageService.sendOutMessage(sendReqBO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,7 +72,8 @@ public class MpAutoReplyServiceImpl implements MpAutoReplyService {
|
||||
: 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) {
|
||||
|
@ -2,11 +2,13 @@ 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.MpAutoReplyDO;
|
||||
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.WxMpXmlOutMessage;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 粉丝消息表 Service 接口
|
||||
*
|
||||
@ -24,20 +26,23 @@ public interface MpMessageService {
|
||||
PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 保存粉丝消息,来自用户发送
|
||||
* 从公众号,接收到用户消息
|
||||
*
|
||||
* @param appId 微信公众号 appId
|
||||
* @param wxMessage 消息
|
||||
*/
|
||||
void createFromUser(String appId, WxMpXmlMessage wxMessage);
|
||||
void receiveMessage(String appId, WxMpXmlMessage wxMessage);
|
||||
|
||||
/**
|
||||
* 创建粉丝消息,通过自动回复
|
||||
* 使用公众号,给用户回复消息
|
||||
*
|
||||
* @param openid 公众号粉丝 openid
|
||||
* @param reply 自动回复
|
||||
* 例如说:自动回复、客服消息、菜单回复消息等场景
|
||||
*
|
||||
* 注意,该方法只是返回 WxMpXmlOutMessage 对象,不会真的发送消息
|
||||
*
|
||||
* @param sendReqBO 消息内容
|
||||
* @return 微信回复消息 XML
|
||||
*/
|
||||
WxMpXmlOutMessage createFromAutoReply(String openid, MpAutoReplyDO reply);
|
||||
WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO);
|
||||
|
||||
}
|
||||
|
@ -2,21 +2,22 @@ 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.framework.common.pojo.PageResult;
|
||||
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.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.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.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.message.bo.MpMessageSendOutReqBO;
|
||||
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;
|
||||
@ -24,20 +25,12 @@ 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.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
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.message.MpMessageMapper;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Validator;
|
||||
import java.io.File;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
/**
|
||||
* 粉丝消息表 Service 实现类
|
||||
*
|
||||
@ -64,13 +57,16 @@ public class MpMessageServiceImpl implements MpMessageService {
|
||||
@Resource
|
||||
private FileApi fileApi;
|
||||
|
||||
@Resource
|
||||
private Validator validator;
|
||||
|
||||
@Override
|
||||
public PageResult<MpMessageDO> getWxFansMsgPage(MpMessagePageReqVO pageReqVO) {
|
||||
return mpMessageMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFromUser(String appId, WxMpXmlMessage wxMessage) {
|
||||
public void receiveMessage(String appId, WxMpXmlMessage wxMessage) {
|
||||
WxMpService mpService = mpServiceFactory.getRequiredMpService(appId);
|
||||
// 获得关联信息
|
||||
MpAccountDO account = mpAccountService.getAccountFromCache(appId);
|
||||
@ -88,24 +84,21 @@ public class MpMessageServiceImpl implements MpMessageService {
|
||||
message.setThumbMediaUrl(mediaDownload(mpService, message.getThumbMediaId()));
|
||||
}
|
||||
mpMessageMapper.insert(message);
|
||||
|
||||
// WxConsts.MenuButtonType.VIEW TODO 芋艿:待测试
|
||||
// wxMessage.getEventKey()
|
||||
|
||||
// WxConsts.MenuButtonType.CLICK
|
||||
// wxMessage.getEventKey()
|
||||
}
|
||||
|
||||
@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());
|
||||
Assert.notNull(account, "公众号账号({}) 不存在", reply.getAppId());
|
||||
MpUserDO user = mpUserService.getUser(reply.getAppId(), openid);
|
||||
Assert.notNull(user, "公众号粉丝({}/{}) 不存在", reply.getAppId(), openid);
|
||||
MpAccountDO account = mpAccountService.getAccountFromCache(sendReqBO.getAppId());
|
||||
Assert.notNull(account, "公众号账号({}) 不存在", sendReqBO.getAppId());
|
||||
MpUserDO user = mpUserService.getUser(sendReqBO.getAppId(), sendReqBO.getOpenid());
|
||||
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());
|
||||
mpMessageMapper.insert(message);
|
||||
|
||||
|
@ -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} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS
|
||||
*/
|
||||
@NotEmpty(message = "消息类型不能为空")
|
||||
public String type;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT
|
||||
*/
|
||||
@NotEmpty(message = "消息内容不能为空", groups = TextGroup.class)
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 媒体 id
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
|
||||
*/
|
||||
@NotEmpty(message = "消息内容不能为空", groups = {ImageGroup.class, VoiceGroup.class, VideoGroup.class})
|
||||
private String mediaId;
|
||||
/**
|
||||
* 媒体 URL
|
||||
*
|
||||
* 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO
|
||||
*/
|
||||
@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;
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user