- 消息类型:即时消息、定时消息。
- 消息通道:钉钉、短信、邮件等。
前言
- 由于业务需求,最近需要做一个通知中心,其中包含短信、钉钉、邮箱、站内信、app推送等不同通道的消息发送,今天简单介绍一下我的实现思路,供大家参考。
需求分析
- 消息类型:即时消息、定时消息。
- 消息通道:钉钉、短信、邮件等。
大致流程
- 不同业务通过统一服务解析模板以及发送通道,然后找到对应的实现发送对应的消息。
实现思路
- 考虑到以后的扩展性,系统内部可以通过MQ方式调用,同时会对外提供接口,如果以后提供给第三方使用可以通过http方式接入。
- 实现思路如上图:不同方式调用统一的服务处理,首先消息落库(便于消息查询以及状态跟踪等)然后校验处理解析模板获取配置信息以及接收主题信息,找到对应的通道通过消息发送的实现发送对应通道的消息,同时记录发送日志以及回写消息记录表的消息状态。(需要提前维护消息配置/模板配置、以及接收主题配置)
表设计
- 初步构想5张表,分别是:
- 消息通道表:记录通道信息以及通道唯一码,用于发送消息时使用。
- 消息配置表:消息编码,消息类型,发送通道(管理通道表)以及默认消息体(支持通配符)等。
- 主题配置表:接收方主题配置,发送消息的要素,例如:手机号、邮箱等,可配可不配,如果上游给到的内容能直接用于消息发送则不需要配置,主要用于某一类的信息获取和发送。
- 消息记录表:记录上游的消息内容以及消息状态等。
- 发送日志表:记录消息的发送内容时间以及结果等。
核心代码设计
- 消息通道设计:策略加模板方法
执行定义
- 定义执行发送消息的方法
/**
* @author leexh
* @since 2022/5/18 19:41
* desc: 执行发送消息定义
*/
public interface MessageChannelService<T> {
void execute(T messageInfo);
}
抽象模板类
/**
* @author leexh
* @since 2022/5/16 16:17
* desc:
*/
@Slf4j
public abstract class BaseMessageChannelProvider<T extends BaseMessage, S extends MessageResult> implements MessageChannelService<T> {
@Resource
private NcMessageRecordService ncMessageRecordService;
/**
* 是否需要获取主题
*/
private boolean isAccessTheme = true;
public void isAccessTheme(boolean isAccessTheme) {
this.isAccessTheme = isAccessTheme;
}
/**
* 获取主题配置
*
* @param messageInfo 主题编码
* @return 解析好的主题信息
*/
protected abstract T getThemeConfig(T messageInfo);
/**
* 发送消息
*
* @param messageInfo 消息信息
* @return 发送结果
*/
protected abstract S sendMessage(T messageInfo);
/**
* 执行消息发送
*
* @param messageInfo 消息信息
*/
public void execute(T messageInfo) {
// 1.获取接收主题
if (isAccessTheme) {
messageInfo = getThemeConfig(messageInfo);
}
// 2.发送消息
S s = sendMessage(messageInfo);
Long messageRecordId = messageInfo.getMessageRecordId();
if (messageRecordId != null) {
NcMessageRecord updateRecord = new NcMessageRecord();
updateRecord.setId(messageRecordId);
// baseDaoInitialService.initialUpdateBaseDaoSystemValue(updateRecord);
// 是否返回结果
updateRecord.setSendStatus(s.getIsBack() ? s.getResultStatus().getCode() : ResultStatus.SENT.getCode());
ncMessageRecordService.updateById(updateRecord);
}
}
}
首先定义BaseMessageChannelProvider抽象类,并实现执行接口,BaseMessageChannelProvider中定义了两个抽象方法需要子类实现,第一个是:getThemeConfig(获取发送主题),第二个是:sendMessage(发送消息),不同通道的主题配置以及发送处理不一样,固有子类自己实现处理;同时定义了两个普通方法:isAccessTheme和execute,子类可实现可不实现;isAccessTheme用于标记是否需要获取主题,如果上游能直接给到接收对象,则可以把isAccessTheme置为false,代表不需要获取主题,方法execute用来执行发送消息以及更新消息状态,可由子类自行实现处理,也可使用父类方法。
钉钉消息处理类
/**
* @author leexh
* @since 2022/5/16 16:38
* desc:
*/
@Slf4j
@Component
public class DingTalkMessageChannelServiceImpl extends BaseMessageChannelProvider<DingTalkMessageInfo, MessageResult> {
@Resource
private NcThemeConfigService themeConfigService;
@Override
protected DingTalkMessageInfo getThemeConfig(DingTalkMessageInfo messageInfo) {
if(log.isDebugEnabled()){
log.debug("获取钉钉模板...");
}
System.out.println("获取钉钉模板...");
return messageInfo;
}
@Override
protected MessageResult sendMessage(DingTalkMessageInfo messageInfo) {
// TODO...
if(log.isDebugEnabled()){
log.debug("发送钉钉消息...");
}
System.out.println("发送钉钉消息...");
isAccessTheme(true);
MessageResult result = new MessageResult();
result.isBack(false);
return result;
}
}
消息体基类
/**
* @author leexh
* @since 2022/5/16 16:34
* desc:
*/
@Data
public class BaseMessage {
/**
* 消息id
*/
private Long messageRecordId;
/**
* 消息编码(对应消息配置)
*/
private String msgCode;
/**
* 模板编码(对应模板配置)
*/
private String themeCode;
public BaseMessage() {
}
public BaseMessage(String msgCode, String themeCode) {
this.msgCode = msgCode;
this.themeCode = themeCode;
}
}
- 可根据实际情况自行扩展和修改,所有通道的消息实体都要基础该类。
发送结果基类
/**
* @author leexh
* @since 2022/5/16 16:33
* desc:
*/
@Data
public class MessageResult {
private ResultStatus resultStatus;
private Boolean isBack = true;
public void isBack(boolean isBack) {
this.isBack = isBack;
}
}
- 每种通道发完消息使用此类或者其子类处理后继逻辑。
发送结果枚举类
/**
* @author leexh
* @since 2022/5/16 21:58
* desc:
*/
@Getter
@AllArgsConstructor
public enum ResultStatus {
/**
* 已发送
*/
SENT("2", "已发送"),
/**
* 发送成功
*/
SUCCESS("3", "发送成功"),
/**
* 发送失败
*/
FAILED("4", "发送失败");
private final String code;
private final String desc;
}
使用demo
public static void main(String[] args) {
// 获取模板配置
// NcPropertyConfig config = new NcPropertyConfig();
// Map<String, MessageChannelService> channelMap = config.getChannelMap();
// MessageChannelService service = channelMap.get("AA");
// DingTalkMessageInfo messageInfo = new DingTalkMessageInfo();
// messageInfo.setMsgCode("AA");
// service.execute(messageInfo);
MessageChannelService<DingTalkMessageInfo> provider = new DingTalkMessageChannelServiceImpl();
DingTalkMessageInfo messageInfo = new DingTalkMessageInfo();
messageInfo.setMsgCode("AA");
((DingTalkMessageChannelServiceImpl) provider).isAccessTheme(true);
provider.execute(messageInfo);
}
运行结果
13:00:26.521 [main] DEBUG com.nc.service.channel.DingTalkMessageChannelServiceImpl - 获取钉钉模板...
获取钉钉模板...
13:00:26.525 [main] DEBUG com.nc.service.channel.DingTalkMessageChannelServiceImpl - 发送钉钉消息...
发送钉钉消息...
Process finished with exit code 0
总结
- 一个消息通知中心的技术难度不大,但是要对接不同的通道以及考虑到后期第三方使用的扩展,想做好做强还要不断改善,结合自身的业务以及消息量来进行设计以及技术方案选择,例如后期需要实现消息推送以及移动端的使用,可以考虑使用微消息队列MQTT等,总之没有最好的只有适合自己的,在满足自己业务需求的情况下做到最简最易使用才更好!
- 本文更多是为了分享实现思路以及设计方案,没有过多的讨论技术,以上代码示例是demo没有具体逻辑,至于不同通道的对接需要自行实现,如有不解或者疑问欢迎评论区指出,谢谢大家!