设计可复用的消息推送模块

2,177 阅读4分钟

领导直接给我派了个任务,几句话就是,开发一个消息推送的模块,可以先暂定邮件推送,后续需要支持其他的渠道,比如公众号,短信,等等

身为一个成熟的工程师,需要自己能够给出设计方案了 ╮(╯_╰)╭

初步的需求分析

  1. 需要考虑多渠道切换或者是扩展为多渠道同步推送,那这个推送的模块,最好设计为继承结构,类似于策略模式,或者是模版模式,大体的推送结构在父类定义好,具体的细节放子类

  2. 推送推送,具体还是要将消息推送给对应的用户,总不可能一有消息,我直接推送给全体用户吧(点名某些喜欢 @所有人 的屑),这里就隐藏了对用户的分组管理

  3. 都聊到对用户分组管理了,那我发消息的话,要发给哪些小组的小伙伴呢?手动硬编码先推送给几个具体的小组成员?等需要调整的时候,再硬编码调整下小组?总感觉很弱智的样子

  4. 分组。。带条件判断。。。,既然这样,那直接给个函数式接口不就得了,通过调用这个小组内的接口来判断是否合适,我可真行

好了,分析结束

画出草图

Screenshot_1.png

先做简单的,搞定几个基础类

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {
	String name;
	String email;
	String phone;
	String wxOpenId; // 关注公众号产生的 openId
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Group {
	String name; // 白班|晚班|紧急联系
	Condition condition;
	List<User> users;
}

@FunctionalInterface
public interface Condition {
	// 判断条件是否满足
	boolean judge();
}

这个 Condition.class 目前能确定的只有3种,就是判断白晚班和紧急联系人,白晚班是根据当前的时间进行判断,那紧急联系人该怎么判断?根据现场,当报警的异常级别较高的话,肯定是需要推送给工程师,这么一分析,就需要引入 ErrorMsg.class,接口也得改造一下

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class ErrorMsg {
	String msg;
	String level;
}

@FunctionalInterface
public interface Condition {
	// 判断条件是否满足
	boolean judge(ErrorMsg msg);
	
	// 超过一定的异常等级则判定为 true
	final Condition ERROR_LEVEL = (ErrorMsg msg) -> {
		return Integer.valueOf(msg.level) > 3 ? true : false;
	};
	// 判断时间在白班区间 [8:00, 20:00)
	final Condition DAY_WORK = (ErrorMsg msg) -> {
		int hour = LocalDateTime.now().getHour();
		return (hour >= 8 && hour < 20) ? true : false;
	};
	// 夜班时间判断
	final Condition NIGHT_WORK = (ErrorMsg msg) -> {
		return !DAY_WORK.judge(msg);
	};
	
}

Notify 推送基类设计

好了,简单的活完了,得考虑下这个消息推送的细节了,比如,该怎么设计才方便使用呢?比如,实例化子类,调用方式就是 sub.send(msg),或者是定义成一个静态的方法,然后 Notify.send(msg)

emmm,想不出来哪种合适,单从调用的角度,静态调用就很舒服了,但静态调用的话,实际上还是需要将需求转给对应的子类,这里就需要考虑一个初始化,以及数据变更的情况了

Notify.class 最终版本的实现代码:

public abstract class Notify {
	static List<Group> groups;
	// 暂定只使用一种
	static Notify type;
	// 处理 groups, type 变更的情况
	public static void init(List<Group> actulGroups, Notify actulType) {
		groups = actulGroups;
		type = actulType;
	}
	
	synchronized public static void sendMsg(ErrorMsg msg) {
		if (ObjectUtils.isEmpty(groups) || ObjectUtils.isEmpty(type)) {
			// 没有初始化的报一个运行异常(懒得捕获)
			throw new RuntimeException("not call Notify.init()");
		}
		List<User> users = new ArrayList<>();
		groups.forEach(g -> {
			// 通过调用 group 自己的 condition 判断,来确认自己小组的用户是否需要通知
			if (g.getCondition().judge(msg)) {
				users.addAll(g.getUsers());
			}
		});
		// 用户去重,避免重复通知
		type.sendToUsers(distinctUser(users), msg);
	}
	// 利用 map 覆盖赋值,去掉重复项
	private static List<User> distinctUser(List<User> users) {
		Map<String, User> map = new HashMap<>();
		users.forEach(u -> map.put(u.getName(), u));
		return new ArrayList<>(map.values());
	}
	// 消息发送的细节,子类去搞定
	protected abstract void sendToUsers(List<User> users, ErrorMsg msg);
}

效果预览

ok,架子搭的差不多了,来实现一个子类 + 测试看看效果

@Component
public class MailNotify extends Notify {
	@Override
	protected void sendToUsers(List<User> users, ErrorMsg msg) {
		System.out.println(users.stream().map(n -> n.getName()).collect(Collectors.joining(", ")) + "|" + msg.getMsg());
	}
}
@Configuration
public class NotifyConfig {
	@Bean
	public void notifyInitDatas() {
		Notify.init(actulGroups(), mailNotify);
	}
	@Bean
	List<Group> actulGroups() {
		User u1 = new User("wangb", "641031***@qq.com", "", ""),
				u2 = new User("bin", "105466***@qq.com", "", ""),
				u3 = new User("wang", "24480***@qq.com", "", "");
		Group g1 = new Group("day", Condition.DAY_WORK, Arrays.asList(u1, u2));
		Group g2 = new Group("night", Condition.NIGHT_WORK, Arrays.asList(u3, u2));
		return Arrays.asList(g1, g2);
	}
	@Autowired
	MailNotify mailNotify;
}
@SpringBootTest
class DemoApplicationTests {
	@Test
	void contextLoads() {
		Notify.sendMsg(new ErrorMsg("this is a test mail", "2"));
	}
}

芜湖~起飞

wangb, bin|this is a test mail

遗漏的邮件模块

是不是忘了点什么,我要做什么来着??

好家伙,我的邮件推送模块呢?


开搞开搞,博客什么的翻一翻,CTRL C/V 完事

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

spring:
  mail:
    host: smtp.exmail.qq.com
    username: ******
    password: ******

标准的 service 安排上,其实也就是在 springboot 下调用 start-mail 里封装好的邮件发送的方法

public interface EmailService {
	/**
	 * 发送简单邮件
	 * @param sendTo 收件人地址
	 * @param titel  邮件标题
	 * @param content 邮件内容
	 */
	public void sendSimpleMail(String sendTo, String titel, String content);
}
@Service
public class EmailServiceImpl implements EmailService {
	@Autowired
	private JavaMailSender mailSender;
	public void sendSimpleMail(String sendTo, String titel, String content) {
		SimpleMailMessage message = new SimpleMailMessage();
		message.setFrom(innorevEmailServer);
		message.setTo(sendTo);
		message.setSubject(titel);
		message.setText(content);
		mailSender.send(message);
	}
	@Value("${spring.mail.username}")
	private String innorevEmailServer;
}

对应的 MailNotify.class 修改下里面的函数调用

@Component
public class MailNotify extends Notify {
	@Override
	protected void sendToUsers(List<User> users, ErrorMsg msg) {
//		System.out.println(users.stream().map(n -> n.getName()).collect(Collectors.joining(", ")) + " " + msg.getMsg());
		String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
		users.forEach(u -> emailService.sendSimpleMail(u.getEmail(), "现场运行异常提醒 " + time, msg.getMsg()));
	}
	@Autowired
	EmailService emailService;
}

邮件发送效果预览

效果图:

Screenshot_2.png

后续需要改造成批量发送,单个发送估计会比较吃资源


完整的流程图

代码修修改改的完成了,那就再画一张完整的流程图

image-20210717150703291.png


原创文章,未经允许,禁止转载

-- by 安逸的咸鱼