.Net Core应用搭建的分布式邮件系统设计
2017-05-24 12:59
501 查看
本篇分享的是由NetCore搭建的分布式邮件系统,主要采用NetCore的Api和控制台应用程序,由于此系统属于公司的所以这里只能分享设计图和一些单纯不设计业务的类或方法;
.NetCore上传多文件的几种示例
开源一个跨平台运行的服务插件 - TaskCore.MainForm
NET Core-学习笔记
Asp.NetCore1.1版本没了project.json,这样来生成跨平台包
![](https://images2015.cnblogs.com/blog/348819/201705/348819-20170524122124529-2119171759.png)
. 邮件发送
. 邮件发送状态的通知(如果需要通知子业务,那么需要通知业务方邮件发送的状态)
. 通知失败处理(自动往绑定的责任人发送一封邮件)
. 填充队列(如果待发邮件队列或者通知队列数据不完整,需要修复队列数据)
邮件请求父类实体:
首先看看MailKit(邮件发送)包,通过安装下载命令: Install-Package MailKit 能够下载最新包,然后你不需要做太花哨的分装,只需要正对于邮件发送的服务器,端口,账号,密码做一些设置基本就行了,如果可以您可以直接使用我的代码:
Redis方面的操作包StackExchange.Redis,现在NetCore支持很多数据库驱动(例如:Sqlserver,mysql,postgressql,db2等)这么用可以参考下这篇文章AspNetCore - MVC实战系列(一)之Sqlserver表映射实体模型,不仅如此还支持很多缓存服务(如:Memorycach,Redis),这里讲到的就是Redis,我利用Redis的list的队列特性来做分布式任务存储,尽管目前我用到的只有一个主Redis服务还没有业务场景需要用到主从复制等功能;这里分享的代码是基于StackExchange.Redis基础上封装对于string,list的操作:
用到Redis的那些操作就添加哪些就行了,也不用太花哨能用就行;
为什么要在公司中首例采用NetCore做开发
为什么要在公司中首例采用NetCore做开发,有些netcoreapi不是还不全面么,您都敢尝试?恐怕会有人这样问我,我只能告诉你NetCore现在出2.0版本了,很多Framwork的常用封装都已经有了,况且她主打的是MVC模式,能够高效的开发系统,也有很多Core的Nuget包支持了,已经到达了几乎可以放心大胆使用的地步,退一万不说有些东西不支持那这又如何,可以采用接口的方式从其他地方对接过来也是一种不错的处理方案。为了让C#这门优秀的语言被广泛应用,默默努力着。目前我写的NetCore方面的文章
AspNetCore - MVC实战系列目录.NetCore上传多文件的几种示例
开源一个跨平台运行的服务插件 - TaskCore.MainForm
NET Core-学习笔记
Asp.NetCore1.1版本没了project.json,这样来生成跨平台包
正片环节 - 分布式邮件系统设计图
![](https://images2015.cnblogs.com/blog/348819/201705/348819-20170524122124529-2119171759.png)
分布式邮件系统说明
其实由上图可以知晓这里我主要采用了Api+服务的模式,这也是现在互联网公司经常采用的一种搭配默认;利用api接受请求插入待发送邮件队列和入库,然后通过部署多个NetCore跨平台服务(这里服务指的是:控制台应用)来做分布式处理操作,跨平台服务主要操作有:. 邮件发送
. 邮件发送状态的通知(如果需要通知子业务,那么需要通知业务方邮件发送的状态)
. 通知失败处理(自动往绑定的责任人发送一封邮件)
. 填充队列(如果待发邮件队列或者通知队列数据不完整,需要修复队列数据)
Api接口的统一验证入口
这里我用最简单的方式,继承Controller封装了一个父级的BaseController,来让各个api的Controller基础统一来做身份验证;来看看重写 public override void OnActionExecuting(ActionExecutingContext context) 的验证代码:public override void OnActionExecuting(ActionExecutingContext context) { base.OnActionExecuting(context); var moResponse = new MoBaseRp(); try { #region 安全性验证 var key = "request"; if (!context.ActionArguments.ContainsKey(key)) { moResponse.Msg = "请求方式不正确"; return; } var request = context.ActionArguments[key]; var baseRq = request as MoBaseRq; //暂时不验证登录账号密码 if (string.IsNullOrWhiteSpace(baseRq.UserName) || string.IsNullOrWhiteSpace(baseRq.UserPwd)) { moResponse.Msg = "登录账号或密码不能为空"; return; } else if (baseRq.AccId <= 0) { moResponse.Msg = "发送者Id无效"; return; } else if (string.IsNullOrWhiteSpace(baseRq.FuncName)) { moResponse.Msg = "业务方法名不正确"; return; } //token验证 var strToken = PublicClass._Md5($"{baseRq.UserName}{baseRq.AccId}", ""); if (!strToken.Equals(baseRq.Token, StringComparison.OrdinalIgnoreCase)) { moResponse.Msg = "Token验证失败"; return; } //验证发送者Id if (string.IsNullOrWhiteSpace(baseRq.Ip)) { var account = _db.EmailAccount.SingleOrDefault(b => b.Id == baseRq.AccId); if (account == null) { moResponse.Msg = "发送者Id无效。"; return; } else { if (account.Status != (int)EnumHelper.EmStatus.启用) { moResponse.Msg = "发送者Id已禁用"; return; } //验证ip var ipArr = account.AllowIps.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); //当前请求的Ip var nowIp = this.GetUserIp(); baseRq.Ip = nowIp; //默认*为所有ip , 匹配ip if (!ipArr.Any(b => b.Equals("*")) && !ipArr.Any(b => b.Equals(nowIp))) { moResponse.Msg = "请求IP为授权"; return; } } } else { var account = _db.EmailAccount.SingleOrDefault(b => b.Id == baseRq.AccId && b.AllowIps.Any(bb => bb.Equals(baseRq.Ip))); if (account == null) { moResponse.Msg = "发送者未授权"; return; } else if (account.Status != (int)EnumHelper.EmStatus.启用) { moResponse.Msg = "发送者Id已禁用"; return; } } //内容非空,格式验证 if (!context.ModelState.IsValid) { var values = context.ModelState.Values.Where(b => b.Errors.Count > 0); if (values.Count() > 0) { moResponse.Msg = values.First().Errors.First().ErrorMessage; return; } } #endregion moResponse.Status = 1; } catch (Exception ex) { moResponse.Msg = "O No请求信息错误"; } finally { if (moResponse.Status == 0) { context.Result = Json(moResponse); } } }
邮件请求父类实体:
/// <summary> /// 邮件请求父类 /// </summary> public class MoBaseRq { public string UserName { get; set; } public string UserPwd { get; set; } /// <summary> /// 验证token(Md5(账号+配置发送者账号信息的Id+Ip)) 必填 /// </summary> public string Token { get; set; } /// <summary> /// 配置发送者账号信息的Id 必填 /// </summary> public int AccId { get; set; } /// <summary> /// 业务方法名称 /// </summary> public string FuncName { get; set; } /// <summary> /// 请求者Ip,如果客户端没赋值,默认服务端获取 /// </summary> public string Ip { get; set; } }
第三方Nuget包的便利
此邮件系统使用到了第三方包,这也能够看出有很多朋友正为开源,便利,NetCore的推广努力着;首先看看MailKit(邮件发送)包,通过安装下载命令: Install-Package MailKit 能够下载最新包,然后你不需要做太花哨的分装,只需要正对于邮件发送的服务器,端口,账号,密码做一些设置基本就行了,如果可以您可以直接使用我的代码:
/// <summary> /// 发送邮件 /// </summary> /// <param name="dicToEmail"></param> /// <param name="title"></param> /// <param name="content"></param> /// <param name="name"></param> /// <param name="fromEmail"></param> /// <returns></returns> public static bool _SendEmail( Dictionary<string, string> dicToEmail, string title, string content, string name = "爱留图网", string fromEmail = "841202396@qq.com", string host = "smtp.qq.com", int port = 587, string userName = "841202396@qq.com", string userPwd = "123123") { var isOk = false; try { if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(content)) { return isOk; } //设置基本信息 var message = new MimeMessage(); message.From.Add(new MailboxAddress(name, fromEmail)); foreach (var item in dicToEmail.Keys) { message.To.Add(new MailboxAddress(item, dicToEmail[item])); } message.Subject = title; message.Body = new TextPart("html") { Text = content }; //链接发送 using (var client = new SmtpClient()) { // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS) client.ServerCertificateValidationCallback = (s, c, h, e) => true; //采用qq邮箱服务器发送邮件 client.Connect(host, port, false); // Note: since we don't have an OAuth2 token, disable // the XOAUTH2 authentication mechanism. client.AuthenticationMechanisms.Remove("XOAUTH2"); //qq邮箱,密码(安全设置短信获取后的密码) ufiaszkkulbabejh client.Authenticate(userName, userPwd); client.Send(message); client.Disconnect(true); } isOk = true; } catch (Exception ex) { } return isOk; }
Redis方面的操作包StackExchange.Redis,现在NetCore支持很多数据库驱动(例如:Sqlserver,mysql,postgressql,db2等)这么用可以参考下这篇文章AspNetCore - MVC实战系列(一)之Sqlserver表映射实体模型,不仅如此还支持很多缓存服务(如:Memorycach,Redis),这里讲到的就是Redis,我利用Redis的list的队列特性来做分布式任务存储,尽管目前我用到的只有一个主Redis服务还没有业务场景需要用到主从复制等功能;这里分享的代码是基于StackExchange.Redis基础上封装对于string,list的操作:
public class StackRedis : IDisposable { #region 配置属性 基于 StackExchange.Redis 封装 //连接串 (注:IP:端口,属性=,属性=) public string _ConnectionString = "127.0.0.1:6377,password=shenniubuxing3"; //操作的库(注:默认0库) public int _Db = 0; #endregion #region 管理器对象 /// <summary> /// 获取redis操作类对象 /// </summary> private static StackRedis _StackRedis; private static object _locker_StackRedis = new object(); public static StackRedis Current { get { if (_StackRedis == null) { lock (_locker_StackRedis) { _StackRedis = _StackRedis ?? new StackRedis(); return _StackRedis; } } return _StackRedis; } } /// <summary> /// 获取并发链接管理器对象 /// </summary> private static ConnectionMultiplexer _redis; private static object _locker = new object(); public ConnectionMultiplexer Manager { get { if (_redis == null) { lock (_locker) { _redis = _redis ?? GetManager(this._ConnectionString); return _redis; } } return _redis; } } /// <summary> /// 获取链接管理器 /// </summary> /// <param name="connectionString"></param> /// <returns></returns> public ConnectionMultiplexer GetManager(string connectionString) { return ConnectionMultiplexer.Connect(connectionString); } /// <summary> /// 获取操作数据库对象 /// </summary> /// <returns></returns> public IDatabase GetDb() { return Manager.GetDatabase(_Db); } #endregion #region 操作方法 #region string 操作 /// <summary> /// 根据Key移除 /// </summary> /// <param name="key"></param> /// <returns></returns> public async Task<bool> Remove(string key) { var db = this.GetDb(); return await db.KeyDeleteAsync(key); } /// <summary> /// 根据key获取string结果 /// </summary> /// <param name="key"></param> /// <returns></returns> public async Task<string> Get(string key) { var db = this.GetDb(); return await db.StringGetAsync(key); } /// <summary> /// 根据key获取string中的对象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public async Task<T> Get<T>(string key) { var t = default(T); try { var _str = await this.Get(key); if (string.IsNullOrWhiteSpace(_str)) { return t; } t = JsonConvert.DeserializeObject<T>(_str); } catch (Exception ex) { } return t; } /// <summary> /// 存储string数据 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expireMinutes"></param> /// <returns></returns> public async Task<bool> Set(string key, string value, int expireMinutes = 0) { var db = this.GetDb(); if (expireMinutes > 0) { return db.StringSet(key, value, TimeSpan.FromMinutes(expireMinutes)); } return await db.StringSetAsync(key, value); } /// <summary> /// 存储对象数据到string /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expireMinutes"></param> /// <returns></returns> public async Task<bool> Set<T>(string key, T value, int expireMinutes = 0) { try { var jsonOption = new JsonSerializerSettings() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; var _str = JsonConvert.SerializeObject(value, jsonOption); if (string.IsNullOrWhiteSpace(_str)) { return false; } return await this.Set(key, _str, expireMinutes); } catch (Exception ex) { } return false; } #endregion #region List操作(注:可以当做队列使用) /// <summary> /// list长度 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public async Task<long> GetListLen<T>(string key) { try { var db = this.GetDb(); return await db.ListLengthAsync(key); } catch (Exception ex) { } return 0; } /// <summary> /// 获取队列出口数据并移除 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public async Task<T> GetListAndPop<T>(string key) { var t = default(T); try { var db = this.GetDb(); var _str = await db.ListRightPopAsync(key); if (string.IsNullOrWhiteSpace(_str)) { return t; } t = JsonConvert.DeserializeObject<T>(_str); } catch (Exception ex) { } return t; } /// <summary> /// 集合对象添加到list左边 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="values"></param> /// <returns></returns> public async Task<long> SetLists<T>(string key, List<T> values) { var result = 0L; try { var jsonOption = new JsonSerializerSettings() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; var db = this.GetDb(); foreach (var item in values) { var _str = JsonConvert.SerializeObject(item, jsonOption); result += await db.ListLeftPushAsync(key, _str); } return result; } catch (Exception ex) { } return result; } /// <summary> /// 单个对象添加到list左边 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="value"></param> /// <returns></returns> public async Task<long> SetList<T>(string key, T value) { var result = 0L; try { result = await this.SetLists(key, new List<T> { value }); } catch (Exception ex) { } return result; } #endregion #region 额外扩展 /// <summary> /// 手动回收管理器对象 /// </summary> public void Dispose() { this.Dispose(_redis); } public void Dispose(ConnectionMultiplexer con) { if (con != null) { con.Close(); con.Dispose(); } } #endregion #endregion }
用到Redis的那些操作就添加哪些就行了,也不用太花哨能用就行;
如何生成跨平台的api服务和应用程序服务
这小节的内容最重要,由于之前有相关的文章,这里就不用再赘述了,来这里看看:Asp.NetCore1.1版本没了project.json,这样来生成跨平台包相关文章推荐
- .Net Core应用搭建的分布式邮件系统设计
- .Net Core应用搭建的分布式邮件系统设计
- .Net Core应用搭建的分布式邮件系统设计
- 解析大型.NET ERP系统 分布式应用模式设计与实现
- 分布式调用跟踪系统的设计和应用
- 分布式调用跟踪系统的设计和应用学习
- 分布式调用跟踪系统的设计和应用学习
- 菜鸟学Linux十:Sendmail服务器的搭建之在Linux和Windows系统上邮件收发的应用
- 流媒体sos rtsp hls h264 高并发 低延时 系统 设计 录像 视频合成 转发 点播 快进 快退 单步播放 分布式集群 服务搭建
- 高性能、高可用、高扩展、分布式微信牌九源码搭建系统架构设计
- 设计数据密集型应用:分布式系统的机遇与挑战
- 设计和应用分布式调用跟踪系统
- ERP系统中邮件提醒定时器框架的设计与应用
- 今日随想——关于企业级应用中分布式架构设计中系统通讯问题
- 应用系统业务撤销设计
- 转 应用系统架构设计
- 应用系统业务撤销设计
- 分布式系统设计套件
- 应用系统架构设计-补全篇(转)
- 应用系统业务撤销设计