背景
今天上午,做了一个可以实现邮件和短信推送的功能,涉及到模板设定,单人推送和多人推送。我觉得有点意思,拿来简单分享一下。
这个需求是这样
浅析
这个需求里,我觉得有两个点需要注意
给多人发送的时候,要在发送时给不同的人发送同一个模板,但实际内容不一样的消息
批量发送不需要在页面停留,可以立刻进行其他的操作
实现
搞清楚关键点之后,就可以动手实践了
消息模板
首先要完成消息模板的设定,模板中需要包含几个关键点,除了独有的一些业务属性,还应该包括,标题,附件,正文信息。
其中,正文里应该可以有一些特殊占位符,用来替换实际的接收人员信息。
这个部分比较简单,代码就不贴了,直接看下效果吧。
这个部分的解体思路,重点就是多附件上传和占位符的使用。
附近上传就是另一种形式的文件上传,需要验证文件类型,md5 验证等。一般做这种管理系统,文件上传的模块都是相对独立的,这里只需要根据实际情况在业务模块进行一个简单的二次封装,就 ok 了。
而占位符,就是模板中替换关键信息的占位信息,在发送邮件的时候,通过替换的形式,就可以完成信息的替换,从而实现给不同的人,发送不同的邮件内容了。
批量发送
这个是我今天想分享的重点。
批量发送,我这里最开始的思路就是非常简单粗暴的遍历检索结果,然后执行模板的 Render 流程,最后再发送到指定的接收人员那里。
然后,这样做肯定是会有问题的,因为邮件发送本身需要调用一些 STMP 的服务,每次执行都会消耗一定的服务端资源,而带来的后果就是管理人员会感到明显的系统卡顿,迟迟收不到服务端的结果信息。
那怎么解决呢?
首先,云服务环境十分成熟的当下,选择第三方的服务来完成推送,是一种非常可靠的解决方案,比如腾讯云有一个邮件推送的服务,除了域名和发信配置,进入到业务流程后,它的步骤和上面提到的模板设置差不多,也是设定关键字作为占位符,调用的时候把占位符当参数传入接口,就可以实现推送效果了。
这种方式还可以设定回调地址,和我们本地的服务器完成数据关联,是一种非常好的推送方案。
但这里我还是采用的企业邮箱发送,也就是本地调用 stmp 服务发送邮件。实现方式,就是通过数据队列!
整体思路是
管理人员设定模板--->选择收信人--->提交发送任务--->队列线程检测到新的任务队列--->执行发送任务
也就是把发送的过程和业务系统进行了解耦。
业务系统只负责提交发送任务,发送过程有队列线程检测并执行。
看下关键代码
编写发送任务
依赖了Coravel中间件,之前也写过类似的实践方案,👉https://xie.infoq.cn/article/5b60e3449daac958a5aeb6252。
这里的场景,还是先编写实际的巡检任务
/// <summary>
/// 信息发送任务
/// </summary>
public class SmsTasks : IInvocable, IInvocableWithPayload<EvaluationExpertSendInfoTask>
{
public EvaluationExpertSendInfoTask Payload { get; set; }
private readonly IExpertEngageRepo _engageRepo;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="engageRepo"></param>
public SmsTasks(IExpertEngageRepo engageRepo)
{
_engageRepo = engageRepo;
}
/// <summary>
/// 启动任务
/// </summary>
/// <returns></returns>
public async Task Invoke()
{
await EmailCheck();
}
//邮件任务巡检
private async Task EmailCheck()
{
if (await RedisHelper.HLenAsync("EmailTask") == 0)
{
Console.WriteLine($"{DateTime.Now},无邮件任务");
await CipAssistant.Logger.writeLogToRedis("无队列邮件需要发送", "info");
return;
}
var rets = RedisHelper.HGetAll("EmailTask");
string msg = $"{DateTime.Now},邮件发送任务启动,共计{rets.Count}封邮件待发送";
Console.WriteLine(msg);
await CipAssistant.Logger.writeLogToRedis(msg, "info");
foreach (var item in rets)
{
string key = item.Key;
var parts = key.Split("|");
int emailId = Convert.ToInt32(parts[0]);
int[] expertIds = Array.ConvertAll(item.Value.Split(','), s => int.Parse(s));
await Task.Run(() => _engageRepo.SendExpertEmail(expertIds, emailId));
await RedisHelper.HDelAsync("EmailTask", key);
await RedisHelper.LPushAsync("EmailTaskEnd", $"{{ timestamp: {parts[1]}, emailId:{emailId}, expertIds:{item.Value} }}");
}
Console.WriteLine($"{DateTime.Now},邮件发送任务结束");
await CipAssistant.Logger.writeLogToRedis($"{DateTime.Now},邮件发送任务结束,共计{rets.Count}封", "info");
}
//短信同理
}
复制代码
依赖注入
任务编写完成后,把发送任务注册成瞬时任务注入到系统
在 ConfigureServices 方法中
services.AddScheduler();
services.AddTransient<SmsTasks>();
services.AddQueue();
复制代码
在 Configure 方法
var provider = app.ApplicationServices;
provider.UseScheduler(scheduler =>
{
scheduler.Schedule<SmsTasks>()
.EveryMinute();
});
复制代码
调用
准备工作完成后,就可以在控制器调用了
/// <summary>
/// 发送邮件
/// </summary>
/// <param name="expertIds"></param>
/// <param name="emailId"></param>
/// <returns></returns>
[HttpPost,ValidateAntiForgeryToken]
public async Task<IActionResult> SendEmail([FromServices] IQueue _queue, int[] expertIds, int emailId)
{
await RedisHelper.HSetAsync("EmailTask", $"{emailId}|{CipAssistant.Utils.DateTimeToTimeStamp(DateTime.Now)}", string.Join(',',expertIds));
_queue.QueueInvocable<Common.SmsTasks>();
return Json(_resp.success($"邮件发送任务已提交到后台任务队列,共计挂起{expertIds.Length}封邮件发送任务,将于1分钟后开始发送,期间您可以进行其他操作"));
}
复制代码
修改完成后,再配合调整一下 UI 的展示逻辑,就可以的到一个非常友好的批量发送的管理界面了。
实际运行的效果如下
其实我这里还是用 Redis 的队列数据结果完成的队列任务,而没有用消息队列中间件来做,整体思路基本都是一样的。
好了,基本就是这样了。
评论