• 消息类型:即时消息、定时消息。
  • 消息通道:钉钉、短信、邮件等。

前言

  • 由于业务需求,最近需要做一个通知中心,其中包含短信、钉钉、邮箱、站内信、app推送等不同通道的消息发送,今天简单介绍一下我的实现思路,供大家参考。

需求分析 

消息中心业务架构图片 消息中心的消息类型_mybatis

 

  • 消息类型:即时消息、定时消息。
  • 消息通道:钉钉、短信、邮件等。

 大致流程

消息中心业务架构图片 消息中心的消息类型_spring_02

 

  • 不同业务通过统一服务解析模板以及发送通道,然后找到对应的实现发送对应的消息。

实现思路 

消息中心业务架构图片 消息中心的消息类型_消息中心业务架构图片_03

 

  • 考虑到以后的扩展性,系统内部可以通过MQ方式调用,同时会对外提供接口,如果以后提供给第三方使用可以通过http方式接入。
  • 实现思路如上图:不同方式调用统一的服务处理,首先消息落库(便于消息查询以及状态跟踪等)然后校验处理解析模板获取配置信息以及接收主题信息,找到对应的通道通过消息发送的实现发送对应通道的消息,同时记录发送日志以及回写消息记录表的消息状态。(需要提前维护消息配置/模板配置、以及接收主题配置)

表设计

消息中心业务架构图片 消息中心的消息类型_mybatis_04

 

  • 初步构想5张表,分别是:
  1. 消息通道表:记录通道信息以及通道唯一码,用于发送消息时使用。
  2. 消息配置表:消息编码,消息类型,发送通道(管理通道表)以及默认消息体(支持通配符)等。
  3. 主题配置表:接收方主题配置,发送消息的要素,例如:手机号、邮箱等,可配可不配,如果上游给到的内容能直接用于消息发送则不需要配置,主要用于某一类的信息获取和发送。
  4. 消息记录表:记录上游的消息内容以及消息状态等。
  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没有具体逻辑,至于不同通道的对接需要自行实现,如有不解或者疑问欢迎评论区指出,谢谢大家!