wenmo8 发布的文章

在一个系统的权限管理模块中,一般都需要跟踪一些具体的日志,ABP框架的系统的日志管理,包括登录日志、接口访问日志、实体变化历史日志,本篇随笔介绍ABP框架中这些日志的管理和界面处理。

1、系统登录日志

在系统每次登录的时候,我们需要记录具体的登录信息,这个一般是系统最基础的日志管理。

ABP框架提供基础的登录日志管理,用户在进行登陆鉴权的时候,会自动记录对应的日志,存储在记录表
AbpUserLoginAttempts
中,我们只需要把它提取出来进行展示即可。

我们在Vue前端,定义BaseApi类,然后继承它实现基础的接口即可。

列表展示的Vue端的JS逻辑代码如下所示。

    getlist() { //列表数据获取
      var param = { //构造常规的分页查询条件
        SkipCount: (this.pageinfo.pageindex - 1) * this.pageinfo.pagesize,
MaxResultCount:
this.pageinfo.pagesize,//过滤条件 UserNameOrEmailAddress: this.searchForm.UserNameOrEmailAddress
};
//使用日期范围选择控件,在查询对象增加开始日期CreationTimeStart、结束日期CreationTimeEnd this.addDateRange(param, this.searchForm.creationTime)//获取产品列表,绑定到模型上,并修改分页数量 this.listLoading = trueloginlog.GetAll(param).then(data=>{this.list =data.result.itemsthis.pageinfo.total =data.result.totalCountthis.listLoading = false})
},

2、接口访问日志

ABP框架是一个后端的Web API框架,因此需要跟踪每个接口的具体访问,包括具体的请求参数和结果等信息,一般我们对接口进行跟踪和优化管理等等。

ABP框架的接口访问日志,也是系统基础提供的记录日志,它在每次系统接口被调用的时候拦截记录,记录信息存储在
AbpAuditLogs
中,同样我们只需要提取出来进行展示即可。

具体的一条接口访问日志查看界面如下所示。

接口访问日志,也就是审计日志的前端调用对象和登录日志的处理类似,只是根据需要增加一些实现的接口。

同样界面的列表获取展示逻辑JS代码如下所示。

    getlist() { //列表数据获取
      var param = { //构造常规的分页查询条件
        SkipCount: (this.pageinfo.pageindex - 1) * this.pageinfo.pagesize,
MaxResultCount:
this.pageinfo.pagesize,//过滤条件 UserNameOrEmailAddress: this.searchForm.UserNameOrEmailAddress
};
//使用日期范围选择控件,在查询对象增加开始日期CreationTimeStart、结束日期CreationTimeEnd this.addDateRange(param, this.searchForm.creationTime)//获取产品列表,绑定到模型上,并修改分页数量 this.listLoading = trueauditlog.GetAll(param).then(data=>{this.list =data.result.itemsthis.pageinfo.total =data.result.totalCountthis.listLoading = false})
},

3、实体修改历史日志

除了上面两种日志外,还有一种是在数据对象发生变化的时候,进行的记录,叫做实体修改(变化)历史记录,记录存放在表AbpEntityChanges、AbpEntityChangeSets和AbpEntityPropertyChanges中。

实体修改历史日志如下界面所示。

打开可以详细查看实体类的属性变化列表,如下界面所示。

实体修改历史记录,系统默认是关闭,需要的话可以在项目模块的配置中打开,如下代码所示即可。

            //配置实体修改历史
            Configuration.EntityHistory.IsEnabled =Constants.EntityHistoryEnabled;//使用基类实现IFullAudited的实体类,记录修改历史
            Configuration.EntityHistory.Selectors.Add(new NamedTypeSelector("IFullAudited",
type
=> typeof(IFullAudited).IsAssignableFrom(type)));

以上这些类型的日志,都是ABP框架基础接口提供的拦截记录日志,我们只需要根据具体的对象获取记录进行展示即可,非常方便。

为了方便读者理解,我列出一下前面几篇随笔的连接,供参考:

循序渐进VUE+Element 前端应用开发(1)--- 开发环境的准备工作

循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用

循序渐进VUE+Element 前端应用开发(3)--- 动态菜单和路由的关联处理

循序渐进VUE+Element 前端应用开发(4)--- 获取后端数据及产品信息页面的处理

循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理


循序渐进VUE+Element 前端应用开发(6)--- 常规Element 界面组件的使用

循序渐进VUE+Element 前端应用开发(7)--- 介绍一些常规的JS处理函数

循序渐进VUE+Element 前端应用开发(8)--- 树列表组件的使用


循序渐进VUE+Element 前端应用开发(9)--- 界面语言国际化的处理

循序渐进VUE+Element 前端应用开发(10)--- 基于vue-echarts处理各种图表展示

循序渐进VUE+Element 前端应用开发(11)--- 图标的维护和使用


循序渐进VUE+Element 前端应用开发(12)--- 整合ABP框架的前端登录处理


循序渐进VUE+Element 前端应用开发(13)--- 前端API接口的封装处理

循序渐进VUE+Element 前端应用开发(14)--- 根据ABP后端接口实现前端界面展示

循序渐进VUE+Element 前端应用开发(15)--- 用户管理模块的处理

循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理

循序渐进VUE+Element 前端应用开发(17)--- 菜单管理

循序渐进VUE+Element 前端应用开发(18)--- 功能点管理及权限控制

VUE+Element 前端应用开发框架功能介绍

循序渐进VUE+Element 前端应用开发(19)--- 后端查询接口和Vue前端的整合

使用代码生成工具快速生成基于ABP框架的Vue+Element的前端界面

循序渐进VUE+Element 前端应用开发(20)--- 使用组件封装简化界面代码

循序渐进VUE+Element 前端应用开发(21)--- 省市区县联动处理的组件使用

循序渐进VUE+Element 前端应用开发(22)--- 简化main.js处理代码,抽取过滤器、全局界面函数、组件注册等处理逻辑到不同的文件中

循序渐进VUE+Element 前端应用开发(23)--- 基于ABP实现前后端的附件上传,图片或者附件展示管理

循序渐进VUE+Element 前端应用开发(24)--- 修改密码的前端界面和ABP后端设置处理

循序渐进VUE+Element 前端应用开发(25)--- 各种界面组件的使用(1)

循序渐进VUE+Element 前端应用开发(26)--- 各种界面组件的使用(2)

电商商品数据库的设计和功能界面的处理

循序渐进VUE+Element 前端应用开发(27)--- 数据表的动态表单设计和数据存储

循序渐进VUE+Element 前端应用开发(28)--- 附件内容的管理

循序渐进VUE+Element 前端应用开发(29)--- 高级查询条件的界面设计

部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用

循序渐进VUE+Element 前端应用开发(30)--- ABP后端和Vue+Element前端结合的分页排序处理

循序渐进VUE+Element 前端应用开发(31)--- 系统的日志管理,包括登录日志、接口访问日志、实体变化历史日志

循序渐进VUE+Element 前端应用开发(32)--- 手机短信动态码登陆处理

循序渐进VUE+Element 前端应用开发(33)--- 邮件参数配置和模板邮件发送处理

使用代码生成工具快速开发ABP框架项目

使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的处理

使用Vue-TreeSelect组件的时候,用watch变量方式解决弹出编辑对话框界面无法触发更新的问题

在一般的系统中,往往也有短信模块的需求,如动态密码的登录,系统密码的找回,以及为了获取用户手机号码的短信确认等等,在ABP框架中,本身提供了对邮件、短信的基础支持,那么只需要根据自己的情况实现对应的接口即可。本篇随笔介绍ABP框架中短信发送处理,包括阿里云短信和普通短信商的短信发送集成。

1、基于第三方阿里云短信的实现

阿里云短信的实现,GitHub上也有一些人实现了一些模块,我们只需要使用对应的模块,然后在Core模块中配置一下依赖即可。

我们一般在做某件事情的时候,先去看看别人是否已经做好了,使用它或者参考它来做事情是个不错的思路。

基于这个道理,我们可以在VS的Nuget包管理中查找一下基于ABP的阿里云短信,可以找到一个合适的进行参考。

这个阿里云的ABP实现适合我们当前的ABP框架版本,因此使用它即可,因此安装引入对应的类库在Core项目中。

在网站
https://github.com/tangyanglai/Sms.Core
我们看到它的使用过程,引入后在项目中启动模块依赖中添加对应的代码即可。

    [DependsOn(typeof(AliyunSmsModule))]

那么我们在项目中的代码如下所示

默认支持两种配置方式,配置文件和SettingManager。下面以配置文件为例,格式为:

{"AliyunSmsSettings": {"AccessKeyId": "","AccessKeySecret": "","SignName": "",           //SendCodeAsync发送验证码使用
    "TemplateCode": "" ,    //SendCodeAsync发送验证码使用
} 
}

根据上面的说明,我们在Host项目的AppSettings.json中增加对应的阿里云配置项,如下所示。

其中AccessKeyId是标识用户身份的ID,AccessKeySecret 是秘钥,SigName是我们申请的短信商户签名,TemplateCode是我们验证码的配置

而短信一般是基于某个模板进行发送的,因此需要确定系统使用的短信模板。

阿里云的发送模块是使用ISmsTemplateSender进行发送的,因此在代码中使用如下所示。

那么在使用发送短信验证码的地方,如AccountService应用层中,使用的时候使用它的注入接口即可发送短信验证码了。

使用发送短信的操作如下所示。

        /// <summary>
        ///发送短信验证码/// </summary>
        /// <param name="phone">手机号码</param>
        /// <param name="code">验证码</param>
        /// <returns></returns>
        public async Task<SmsResult> SendCodeAsync(string phone, stringcode)
{
return await_smsTemplateSender.SmsService.SendCodeAsync(phone, code);

}
/// <summary> ///发送模板消息/// </summary> /// <param name="input">模板对象</param> /// <returns></returns> public async Task<SmsResult>SendTemplateMessageAsync(SendTemplateMessageInput input)
{
return await_smsTemplateSender.SmsService.SendTemplateMessageAsync(input);
}

2、使用自己的阿里云短信发送封装

我之前随笔《
使用阿里云的短信服务发送短信
》中写过如何处理阿里云短信,虽然那个是常规.net framework的程序中集成的,不过在.net Core的代码都是差不多的。

我们知道ABP框架提供了对应的短信发送接口,一般注入在系统中使用即可。

namespaceMyProject.Net
{
/// <summary> ///短信发送接口/// </summary> public interfaceISmsSender
{
Task
<CommonResult> SendAsync(string number, stringmessage);
}
}

那么我们自己定义的短信发送接口,实现它即可,然后注入使用对应的接口即可。

根据阿里云接口需求,定义一个类似的模型用作加载参数的。

    /// <summary>
    ///阿里云配置参数/// </summary>
    internal classAliyunSmsSettting
{
public string AccessKeyId { get; set; }public string AccessKeySecret { get; set; }public string RegionId { get; set; }public string EndpointName { get; set; }public string Domain { get; set; }public string Product { get; set; }public string SignName { get; set; }public string TemplateCode { get; set; }public string TemplateParam { get; set; }
}

然后让我们的接口实现函数,初始化的时候获取对应的配置信息供使用。

{/// <summary>
    ///使用简单封装,不依赖其他外部模块的阿里云短信发送/// </summary>
    public classAliyunSmsSender : IShouldInitialize, ISmsSender, ITransientDependency
{
public IConfiguration AppConfiguration { get; set; }public IIocManager IocManager { get; set; }public ILogger Logger { get; set; }private const string Key = "AliyunSmsSettings";private const string endpoint = "dysmsapi.aliyuncs.com";/// <summary> ///短信配置信息/// </summary> private AliyunSmsSettting SmsSettings { get; set; }publicAliyunSmsSender(IConfiguration appConfiguration, IIocManager iocManager)
{
this.AppConfiguration =appConfiguration;this.IocManager =iocManager;this.Logger =NullLogger.Instance;
}
public voidInitialize()
{
this.SmsSettings = GetConfigFromConfigOrSettingsByKey<AliyunSmsSettting>().Result;
}

然后根据我之前随笔的实现逻辑,给他实现对应的发送操作即可,部分关键代码如下所示

        /// <summary>
        ///发送短信/// </summary>
        /// <param name="number">手机号码</param>
        /// <param name="message">消息或验证码</param>
        /// <returns></returns>
        public async Task<CommonResult> SendAsync(string number, stringmessage)
{
var result = awaitPrivateSend(number, message);returnresult;
}
/// <summary> ///发送逻辑/// </summary> /// <returns></returns> private async Task<CommonResult> PrivateSend(string number, stringcode)
{
string nowDate = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");//GTM时间 var keyValues = new Dictionary<string, string>();//声明一个字典//1.系统参数 keyValues.Add("SignatureMethod", "HMAC-SHA1");
keyValues.Add(
"SignatureNonce", Guid.NewGuid().ToString());
keyValues.Add(
"AccessKeyId", this.SmsSettings.AccessKeyId);
keyValues.Add(
"SignatureVersion", "1.0");
keyValues.Add(
"Timestamp", nowDate);
keyValues.Add(
"Format", "Json");//可换成xml//2.业务api参数 keyValues.Add("Action", "SendSms");
keyValues.Add(
"Version", "2017-05-25");
keyValues.Add(
"RegionId", "cn-hangzhou");
keyValues.Add(
"PhoneNumbers", number);
keyValues.Add(
"SignName", this.SmsSettings.SignName);
keyValues.Add(
"TemplateCode", this.SmsSettings.TemplateCode);
keyValues.Add(
"TemplateParam", string.Format("{{\"code\":\"{0}\"}}", code));
keyValues.Add(
"OutId", "123");//3.去除签名关键字key if (keyValues.ContainsKey("Signature"))
{
keyValues.Remove(
"Signature");
}
//4.参数key排序 Dictionary<string, string> ascDic = keyValues.OrderBy(o => o.Key).ToDictionary(o => o.Key, p =>p.Value.ToString());//5.构造待签名的字符串 var builder = newStringBuilder();foreach (var item inascDic)
{
if (item.Key == "SignName")
{
}
else{
builder.Append(
"&").Append(specialUrlEncode(item.Key)).Append("=").Append(specialUrlEncode(item.Value));
}
if (item.Key == "RegionId")
{
builder.Append(
"&").Append(specialUrlEncode("SignName")).Append("=").Append(specialUrlEncode(keyValues["SignName"]));
}
}
string sorteQueryString = builder.ToString().Substring(1);

StringBuilder stringToSign
= newStringBuilder();
stringToSign.Append(
"GET").Append("&");
stringToSign.Append(specialUrlEncode(
"/")).Append("&");
stringToSign.Append(specialUrlEncode(sorteQueryString));
string Sign = MySign(this.SmsSettings.AccessKeySecret + "&", stringToSign.ToString());//6.签名最后也要做特殊URL编码 string signture =specialUrlEncode(Sign);//最终打印出合法GET请求的URL string url = string.Format("http://{0}/?Signature={1}{2}", endpoint, signture, builder);var modal = awaitGetHtmlResult(url);return newCommonResult(modal.Success, modal.Message);
}

然后在Core模块中初始化的时候,替换对应的短信发送实现即可。

这样就可以使用我们自己的短信接口了

发送代码如下所示

        /// <summary>
        ///发送短信验证码/// </summary>
        /// <param name="phone">手机号码</param>
        /// <param name="code">验证码</param>
        /// <returns></returns>
        public async Task<CommonResult> SendSmsCodeAsync(string phone, stringcode)
{
return await _smsSender.SendAsync(phone, code); //使用阿里云接口 }

3、普通短信商的短信发送集成

还有一种我们可能不是基于阿里云,而是其他提供商的接口发送,操作也是自定义短信接口的封装。

我们使用如下参数来确定短信提供商的信息,也可以根据需要自己调整。

定义一个配置对应的配置对象,方便获取参数信息。

    /// <summary>
    ///自定义短信配置/// </summary>
    internal classMySmsSettings
{
/// <summary> ///供应商代码/// </summary> public string spcode { get; set; }/// <summary> ///账户/// </summary> public string username { get; set; }/// <summary> ///密码/// </summary> public string password { get; set; }
}

由于我们这个的实现也是基于标准接口ISmsSender的,那么我们实现这个后,也需要特定指定这个实现为ISmsSender的使用。

例如在CoreModule中替换为这个短信实现的话,如下代码。

   //使用自定义的 ISmsSender
   Configuration.ReplaceService<ISmsSender, MySmsSender>();

使用接口发送短信的时候,就和我们上面的操作类似的了。

在一些系统中,有时候用户忘记密码,可以通过向自己手机发送动态验证码的方式实现系统登录功能。本篇随笔介绍如何结合后端ABP框架的短信发送和缓存模块的处理,实现手机短信动态码登陆处理。

一般的登录方式,分为普通账号登录,动态密码登陆,扫描二维码登录等几种方式,其他方式这里不讲,主要介绍动态码登录方式。

1、短信验证码的发送处理

我在上篇随笔《
ABP框架中短信发送处理,包括阿里云短信和普通短信商的短信发送集成
》中介绍了如何使用ABP框架实现短信的发送处理,因此我们前后端通过短信的方式,可以实现动态密码的登陆处理。

因此在授权登陆的控制器中,我们增加短信发送的接口注入使用,如下所示。

然后通过定义两个接口,一个是发送动态验证码给用户手机的接口,一个是根据用户手机和动态验证码的方式进行登录处理接口。

然后我们在这个验证身份的控制器上增加两个方法即可。

用例也就是分了两个处理方法。

在处理发送短信验证码之前,我们来介绍一下短信验证码的处理规则,我们发送短信成功后,把验证码存在系统缓存里面,一般系统缓存是存放在Redis里面,缓存需要一个键和定义好的类对象进行存储。

我们定义好存储的对象类,再在系统中使用即可。

    /// <summary>
    ///短信登陆动态密码缓存对象/// </summary>
[Serializable]public classSmsLoginCodeCacheItem
{
public const string CacheName = "AppSmsLoginCodeCacheItem";public string Code { get; set; }public string PhoneNumber { get; set; }publicSmsLoginCodeCacheItem()
{
}
public SmsLoginCodeCacheItem(string code, stringphone)
{
Code
=code;
PhoneNumber
=phone;
}
}

我们可以在系统模块初始化的时候,配置好缓存对应的失效时间,如下所示。

            //配置SMS登录动态码有效期限
            Configuration.Caching.Configure(SmsLoginCodeCacheItem.CacheName, cache =>{
cache.DefaultSlidingExpireTime
=TimeSpan.FromMinutes(Constants.SmsCodeExpiredMinutes);
});

发送短信验证码作为动态密码的逻辑代码如下所示。

        /// <summary>
        ///发送登录动态码/// </summary>
        /// <param name="model">手机登录动态码</param>
        /// <returns></returns>
[HttpPost]public async Task<CommonResult>SendPhoneLoginSmsCode([FromBody] AuthenticateByPhoneCaptchaModel model)
{
//获取随机6位数字动态验证码 var code = RandomHelper.GetRandom(100000, 999999).ToString();//使用自定义模板处理短信发送 string message = string.Format(Constants.MySmsCodeTemplate, code);var result = await_smsSender.SendAsync(model.PhoneNumber, message);if(result.Success)
{
var cacheKey = model.PhoneNumber;//以手机号码作为键存储验证码缓存 var cacheItem = new SmsLoginCodeCacheItem { Code = code, PhoneNumber =model.PhoneNumber };var cache = _cacheManager.GetCache<string, SmsLoginCodeCacheItem>(SmsLoginCodeCacheItem.CacheName);
cache.Set(cacheKey, cacheItem);
}
returnresult;
}

我们还需要在前端中设计一个使用动态短信码登录的界面,如下所示。

短信发送成功,可以在用户手机查看对应的动态码。

验证码发送后,我们也可以在Redis中看到对应的数据,如下所示。

2、动态码登录处理

发送了短信码后,系统在缓存中存放一段时间的数据,如果在这个期间进行登录,会根据缓存进行匹配,如果匹配成功,那么就进行相关登录身份的处理即可。

系统登录验证的处理代码如下所示。

        /// <summary>
        ///通过手机验证码授权/// </summary>
        /// <param name="model">手机验证码Dto</param>
        /// <returns></returns>
[HttpPost]public async Task<AuthenticateResultModel>AuthenticateByPhoneCaptcha([FromBody] AuthenticateByPhoneCaptchaModel model)
{
var loginResult = awaitGetLoginResultByPhoneCaptchaAsync(
model.PhoneNumber,
model.SmsCode,
GetTenancyNameOrNull()
);
//if(loginResult.Result == AbpLoginResultType.Success)//这里成功,移除短信缓存 var cache = _cacheManager.GetCache<string, SmsLoginCodeCacheItem>(SmsLoginCodeCacheItem.CacheName);
cache.Remove(model.PhoneNumber);
//移除缓存短信键值 var accessToken =CreateAccessToken(CreateJwtClaims(loginResult.Identity));return newAuthenticateResultModel
{
AccessToken
=accessToken,
ExpireInSeconds
= (int)_configuration.Expiration.TotalSeconds,
EncryptedAccessToken
=GetEncryptedAccessToken(accessToken),
UserId
=loginResult.User.Id
};
}

这里主要的逻辑封装在 GetLoginResultByPhoneCaptchaAsync 中,这个登录的方式可以参考ABP框架基础的登陆方式进行改动即可。

        /// <summary>
        ///获取登陆结果通过手机验证码/// </summary>
        /// <param name="phoneNumber">手机号</param>
        /// <param name="captcha">验证码</param>
        /// <param name="tenancyName">租户名</param>
        /// <returns></returns>
        private async Task<AbpLoginResult<Tenant, User>> GetLoginResultByPhoneCaptchaAsync(string phoneNumber, string captcha, stringtenancyName)
{
var loginResult = await_logInManager.LoginByMobileAsync(phoneNumber, captcha, tenancyName);switch(loginResult.Result)
{
caseAbpLoginResultType.Success:returnloginResult;default:throw_abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(loginResult.Result, loginResult.User.UserName, tenancyName);
}
}

参照ABP框架基础的登陆授权方式,我们在UserManager中增加类似的验证码登陆管理方式,如下所示。

前端在处理相关发送验证码和登录授权的操作,是针对API的调用,因此需要封装对应的API处理。

然后仿照常规登录的处理,编写一个动态码登录的处理方式,放在对应的Module中即可。

  dynamiclogin({ commit }, userInfo) { //动态密码登陆
    const { mobile, smscode } =userInforeturn new Promise((resolve, reject) =>{
tokenauth.AuthenticateByPhoneCaptcha({ phoneNumber: mobile.trim(), smsCode: smscode }).then(response
=>{
const { result }
= response //获取返回对象的 result //console.log(result)// 记录数据 var token = result.accessToken //用户令牌 var userId = result.userId //用户id //修改State对象,记录令牌和用户Id commit('SET_TOKEN', token)
commit(
'SET_USERID', userId)//存储cookie setToken(token)
setUserId(userId)

resolve()
}).
catch(error =>{
reject(error)
})
})
},

在登录界面中,输入动态码登录即可顺利进入系统,和常规的处理一样。

以上就是参照常规账号密码登录的方式,构建一个动态码登录的处理,流程还是差不多,不过整合了短信发送,缓存处理,账号登陆等几个流程,可以作为一个简单的系统登录过程的了解。

为了方便读者理解,我列出一下前面几篇随笔的连接,供参考:

循序渐进VUE+Element 前端应用开发(1)--- 开发环境的准备工作

循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用

循序渐进VUE+Element 前端应用开发(3)--- 动态菜单和路由的关联处理

循序渐进VUE+Element 前端应用开发(4)--- 获取后端数据及产品信息页面的处理

循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理


循序渐进VUE+Element 前端应用开发(6)--- 常规Element 界面组件的使用

循序渐进VUE+Element 前端应用开发(7)--- 介绍一些常规的JS处理函数

循序渐进VUE+Element 前端应用开发(8)--- 树列表组件的使用


循序渐进VUE+Element 前端应用开发(9)--- 界面语言国际化的处理

循序渐进VUE+Element 前端应用开发(10)--- 基于vue-echarts处理各种图表展示

循序渐进VUE+Element 前端应用开发(11)--- 图标的维护和使用


循序渐进VUE+Element 前端应用开发(12)--- 整合ABP框架的前端登录处理


循序渐进VUE+Element 前端应用开发(13)--- 前端API接口的封装处理

循序渐进VUE+Element 前端应用开发(14)--- 根据ABP后端接口实现前端界面展示

循序渐进VUE+Element 前端应用开发(15)--- 用户管理模块的处理

循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理

循序渐进VUE+Element 前端应用开发(17)--- 菜单管理

循序渐进VUE+Element 前端应用开发(18)--- 功能点管理及权限控制

VUE+Element 前端应用开发框架功能介绍

循序渐进VUE+Element 前端应用开发(19)--- 后端查询接口和Vue前端的整合

使用代码生成工具快速生成基于ABP框架的Vue+Element的前端界面

循序渐进VUE+Element 前端应用开发(20)--- 使用组件封装简化界面代码

循序渐进VUE+Element 前端应用开发(21)--- 省市区县联动处理的组件使用

循序渐进VUE+Element 前端应用开发(22)--- 简化main.js处理代码,抽取过滤器、全局界面函数、组件注册等处理逻辑到不同的文件中

循序渐进VUE+Element 前端应用开发(23)--- 基于ABP实现前后端的附件上传,图片或者附件展示管理

循序渐进VUE+Element 前端应用开发(24)--- 修改密码的前端界面和ABP后端设置处理

循序渐进VUE+Element 前端应用开发(25)--- 各种界面组件的使用(1)

循序渐进VUE+Element 前端应用开发(26)--- 各种界面组件的使用(2)

电商商品数据库的设计和功能界面的处理

循序渐进VUE+Element 前端应用开发(27)--- 数据表的动态表单设计和数据存储

循序渐进VUE+Element 前端应用开发(28)--- 附件内容的管理

循序渐进VUE+Element 前端应用开发(29)--- 高级查询条件的界面设计

部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用

循序渐进VUE+Element 前端应用开发(30)--- ABP后端和Vue+Element前端结合的分页排序处理

循序渐进VUE+Element 前端应用开发(31)--- 系统的日志管理,包括登录日志、接口访问日志、实体变化历史日志

循序渐进VUE+Element 前端应用开发(32)--- 手机短信动态码登陆处理

循序渐进VUE+Element 前端应用开发(33)--- 邮件参数配置和模板邮件发送处理

使用代码生成工具快速开发ABP框架项目

使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的处理

使用Vue-TreeSelect组件的时候,用watch变量方式解决弹出编辑对话框界面无法触发更新的问题

在系统处理中,有时候需要发送邮件通知用户,如新增用户的邮件确认,密码找回,以及常规订阅消息、通知等内容处理,都可以通过邮件的方式进行处理。本篇随笔介绍结合VUE+Element 前端,实现系统的邮件参数配置管理,以及基于邮件模板的方式进行邮件的发送。

1、邮件参数的配置管理

邮件参数一般需要配置如用户名、密码、邮件地址,显示名称,以及其他邮件所需的必要配置,一般我们可以通过界面管理的方式进行常规的参数配置,如下界面所示。

其中的登录密码,现在一般是授权登录密码,而不是原始的账号密码了。以163为例,可以在设置中添加一个授权密码。

前端参数的配置管理,我们构建一个Api类,用于调用ABP后端的Api接口,如下所示。

查看窗体中显示邮件参数数据的代码如下所示。

showAbpEmail() {this.resetForm('emailForm')
setting.GetEmailSettingsForApplication().then(data
=>{if(data.result) {
Object.assign(
this.abpEmailForm, data.result)
}
this.isAbpEmail = true //编辑状态 })
},

2、ABP后端邮件的发送处理

系统参数配置完成后,我们需要根据这些邮件参数进行邮件的发送,ABP框架基于.net core 的实现,我们发送邮件,需要添加一个Abp.MailKit的依赖,如下所示。

然后在模块的依赖上,添加对应的AbpMailkitModule的依赖即可。

一般来说,我们发送邮件,还需要重写DefaultMailKitSmtpBuilder的配置处理项,以便自定义发送处理过程。

    /// <summary>
    ///重写默认的SmtpBuilder类/// </summary>
    public classMyMailKitSmtpBuilder : DefaultMailKitSmtpBuilder
{
private readonlyISmtpEmailSenderConfiguration _smtpEmailSenderConfiguration;private readonlyIAbpMailKitConfiguration _abpMailKitConfiguration;publicMyMailKitSmtpBuilder(ISmtpEmailSenderConfiguration smtpEmailSenderConfiguration, IAbpMailKitConfiguration abpMailKitConfiguration)
:
base(smtpEmailSenderConfiguration, abpMailKitConfiguration)
{
_smtpEmailSenderConfiguration
=smtpEmailSenderConfiguration;
_abpMailKitConfiguration
=abpMailKitConfiguration;
}
/// <summary> ///配置发送处理/// </summary> /// <param name="client"></param> protected override voidConfigureClient(SmtpClient client)
{
client.CheckCertificateRevocation
= false;
client.ServerCertificateValidationCallback
= (sender, certificate, chain, errors) => true;

client.Connect(_smtpEmailSenderConfiguration.Host, _smtpEmailSenderConfiguration.Port, GetSecureSocketOption());
if(_smtpEmailSenderConfiguration.UseDefaultCredentials)
{
return;
}
var username =_smtpEmailSenderConfiguration.UserName;var password =_smtpEmailSenderConfiguration.Password;//password = SimpleStringCipher.Instance.Decrypt(originalPass);//如果不重写自定义配置MySmtpEmailSenderConfiguration,那么密码需要解密 client.Authenticate(username, password);//base.ConfigureClient(client); }
}

但是由于基类处理中,默认的邮件配置密码是直接从数据库读取信息的,没有进行加密,如基类SmtpEmailSenderConfiguration的实现如下。

但是我们应用程序的设置信息,密码是经过加密过的,因此需要重写这个配置项,进行对应的密码解密。

    /// <summary>
    ///邮件发送参数配置类/// </summary>
    public classMySmtpEmailSenderConfiguration : SmtpEmailSenderConfiguration
{
public MySmtpEmailSenderConfiguration(ISettingManager settingManager) : base(settingManager)
{

}
/// <summary> ///重写密码处理,需要解密密码/// </summary> public override string Password=>SimpleStringCipher.Instance.Decrypt(GetNotEmptySettingValue(EmailSettingNames.Smtp.Password));
}

最后,在Module中初始化中处理下对应的自定义发送和自定义配置项的处理类。

这样默认注入的发送邮件的接口就正常了,我们接下来就是根据邮件的模板进行内容发送即可。

3、邮件模板的处理

一般的邮件模板,是一个独立的文件方式,文件中定义一些预设的内容,然后实际处理的时候,替换这些变量即可。

邮件模板的内容替换,我喜欢用基于模板引擎的方式处理变量的替换,一般我用NVelocity来进行处理。

我在早期介绍过一些关于NVelocity的知识,需要可以参考:


使用NVelocity生成内容的几种方式


强大的模板引擎开源软件NVelocity》


Database2Sharp版本更新之自定义模板生成


使用NVelocity0.5实现服务器端页面自动生成

例如,我在文本中定义一个连接:http://www.iqidi.com/h5/EmailValidate?callback=${callback} 其中${callback} 就是变量定义,可以在运行中进行变量替换的。

我们在Host项目中定义一些邮件文件模板,如下所示。

然后在使用的应用服务类中注入对应的邮件发送接口以供使用。

具体的邮件发送,就是读取模板内容,进行替换变量,然后调用接口进行邮件的发送即可。

        /// <summary>
        ///发送校验的电子邮件/// </summary>
        /// <returns></returns>
        public asyncTask SendEmailValidate(SendEmailActivationLinkDto input)
{
var user = awaitGetUserByChecking(input.EmailAddress);//user.SetNewEmailConfirmationCode(); #region 根据模板生成邮件正文 //使用相对路径进行构造处理 string template = string.Format("/UploadFiles/Email/EmailValidate.html");var helper = newNVelocityHelper(template);//${标题} ${内容} ${称呼} ${日期} var tilte = "邮箱验证通知";
helper.AddKeyValue(
"title", tilte);
helper.AddKeyValue(
"callname", "");
helper.AddKeyValue(
"date", DateTime.Now.ToLongDateString());//邮箱校验码 var callback =Guid.NewGuid().ToString();
helper.AddKeyValue(
"callback", callback);var html =helper.ExecuteString();#endregion #region 构建邮件内容对象 发送邮件 string toEmail = "wuhuacong@163.com";await _emailSender.SendAsync(newSystem.Net.Mail.MailMessage
{
To
={ toEmail },
Subject
=tilte,
Body
=html,
IsBodyHtml
= true});#endregionLogHelper.Logger.Info(string.Format("校验邮件发送给:{0}, {1}", toEmail, "发送邮件成功"));
}

为了处理模板内容的方便,我们把规则放在辅助类 NVelocityHelper 中处理即可。

上面就是整个发送模板邮件的过程代码了。

发送邮件成功后,我们可以在邮箱中查看到对应的邮件,我们一般根据自己的业务需要定义不同的邮件模板即可。

邮件发送成功后,查看邮件效果如下所示。

为了方便读者理解,我列出一下前面几篇随笔的连接,供参考:

循序渐进VUE+Element 前端应用开发(1)--- 开发环境的准备工作

循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用

循序渐进VUE+Element 前端应用开发(3)--- 动态菜单和路由的关联处理

循序渐进VUE+Element 前端应用开发(4)--- 获取后端数据及产品信息页面的处理

循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理


循序渐进VUE+Element 前端应用开发(6)--- 常规Element 界面组件的使用

循序渐进VUE+Element 前端应用开发(7)--- 介绍一些常规的JS处理函数

循序渐进VUE+Element 前端应用开发(8)--- 树列表组件的使用


循序渐进VUE+Element 前端应用开发(9)--- 界面语言国际化的处理

循序渐进VUE+Element 前端应用开发(10)--- 基于vue-echarts处理各种图表展示

循序渐进VUE+Element 前端应用开发(11)--- 图标的维护和使用


循序渐进VUE+Element 前端应用开发(12)--- 整合ABP框架的前端登录处理


循序渐进VUE+Element 前端应用开发(13)--- 前端API接口的封装处理

循序渐进VUE+Element 前端应用开发(14)--- 根据ABP后端接口实现前端界面展示

循序渐进VUE+Element 前端应用开发(15)--- 用户管理模块的处理

循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理

循序渐进VUE+Element 前端应用开发(17)--- 菜单管理

循序渐进VUE+Element 前端应用开发(18)--- 功能点管理及权限控制

VUE+Element 前端应用开发框架功能介绍

循序渐进VUE+Element 前端应用开发(19)--- 后端查询接口和Vue前端的整合

使用代码生成工具快速生成基于ABP框架的Vue+Element的前端界面

循序渐进VUE+Element 前端应用开发(20)--- 使用组件封装简化界面代码

循序渐进VUE+Element 前端应用开发(21)--- 省市区县联动处理的组件使用

循序渐进VUE+Element 前端应用开发(22)--- 简化main.js处理代码,抽取过滤器、全局界面函数、组件注册等处理逻辑到不同的文件中

循序渐进VUE+Element 前端应用开发(23)--- 基于ABP实现前后端的附件上传,图片或者附件展示管理

循序渐进VUE+Element 前端应用开发(24)--- 修改密码的前端界面和ABP后端设置处理

循序渐进VUE+Element 前端应用开发(25)--- 各种界面组件的使用(1)

循序渐进VUE+Element 前端应用开发(26)--- 各种界面组件的使用(2)

电商商品数据库的设计和功能界面的处理

循序渐进VUE+Element 前端应用开发(27)--- 数据表的动态表单设计和数据存储

循序渐进VUE+Element 前端应用开发(28)--- 附件内容的管理

循序渐进VUE+Element 前端应用开发(29)--- 高级查询条件的界面设计

部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用

循序渐进VUE+Element 前端应用开发(30)--- ABP后端和Vue+Element前端结合的分页排序处理

循序渐进VUE+Element 前端应用开发(31)--- 系统的日志管理,包括登录日志、接口访问日志、实体变化历史日志

循序渐进VUE+Element 前端应用开发(32)--- 手机短信动态码登陆处理

循序渐进VUE+Element 前端应用开发(33)--- 邮件参数配置和模板邮件发送处理

使用代码生成工具快速开发ABP框架项目

使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的处理

使用Vue-TreeSelect组件的时候,用watch变量方式解决弹出编辑对话框界面无法触发更新的问题

整理一下自己之前的Winform开发要点,以图文的方式展示一些关键性的技术特点,总结一下。

1、主体界面布局

2、权限管理系统

3、工作流模块

4、字典管理

5、通用的附件管理模块

6、系统模块化开发和集成

模块化类似小孩子搭积木的方式构建一个所要的形状,虽然软件和建筑在这方面肯定更加复杂化,但是模块化系统是大势所趋,也是简化开发、易于维护、提高系统健壮性的重要举措。

数据库方面

支持多种数据库,支持分拆数据库,同时使用多个数据库等方式。

Winform混合框架的开发架构

7、Database2Sharp代码生成工具辅助快速开发

8、报表打印和处理

整合使用FastReport、XtraReport、RDLC、Grid++等报表展示方式。

9、串口开发及Modbus

封装串口接口的开发,以及Modbus串口和网络连接的处理。

10、SignalR通讯开发

11、TX TextControl控件使用

12、配置管理界面

13、Web API的访问和接口处理

14、综合案例

提供各种各样的开发测试案例代码

以上是我整理这几年来写Winform开发框架的技术方面图片,都是来自我Winform开发框架或者Winform混合式开发框架中的特性总结,基本覆盖了重要Winform开发的技术要点和方向,不足之处希望大家指正。