2023年2月

前面介绍了很多关于ABP框架的后台Web API 服务端,以及基于Vue+Element前端应用,本篇针对两者的联合部署,以及对部署中遇到的问题进行处理。ABP框架的后端是基于.net core5.0 的Asp.net core 应用,因此和常规的Asp.net core 应用部署一样;而Vue+Element前端应用则是基于nodejs的应用,部署方式又有所不同,这里介绍基于Nginx的部署。

1、部署基于.netcore5.0的ABP框架后台Api服务端

1)安装.net core 环境

在部署asp.net core服务前,需要在服务器中安装必须的环境。

由于当前ABP的Web API是基于.net core 5的,因此,我们打开.net core 5的页面:
https://dotnet.microsoft.com/download/dotnet/5.0

在这里选择下载Hosting Bundle。因为Hosting Bundle包括了.Net Core运行时和IIS支持。下载完成以后,双击exe文件即可进行安装即可。

安装完成以后我们在命令行里面输入下面的命令,检查是否安装成功:

dotnet --info

如下图所示:

可以看到提示我们已经安装了.NET Core runtimes环境等所需软件。

由于我们需要部署到IIS上面,所以安装完需使用下面的命令行重启IIS服务:

net stop was /y
net start w3svc

如下图所示:

也可以在管理服务器里面重启启动IIS。

2) 发布.net core项目

服务器.net core环境弄好后,下一步就是准备好发布包,我们在ABP框架的Host项目进行发布。

发布选择文件发布,如下所示。

然后调整设置如下所示。

最后我们生成发布包,在对应的目录下,如下所示。

G:\***\Web.Host\bin\Release\net5.0\publish\

3)在服务器中设置IIS

把文件上传到服务器上,然后就是准备设置好IIS了。

先在IIS服务器上创建一个网站,指定对应目录和端口等信息,如下所示。

然后找到对应的应用程序池,找到刚才创建的ABP应用程序池。设置.net clr版本为【
无托管代码

在其中把标识由
ApplicationPoolIdentity
修改为
LocalSystem
,以提供应用权限可以访问数据库。

最后点击【确定】按钮,网站及部署完成。我们就可以在浏览器里面进行浏览了。

顺利弄完asp.net core的后端API服务,那么下面就需要同时把Vue+Element的前端部署在服务端了。

2、使用Nginx部署Vue+Element前端应用

部署Vue+Element的前端应用,建议使用Nginx服务,这个对于Vue里面的URL代理转向设置比较方便些。

Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器。

首先到nginx服务网站下载对应的程序包进行安装:
http://nginx.org/en/download.html
,  建议下载稳定版本进行安装。

nginx的DOS操作命令有几个,比较简单

startnginx    启动

nginx -s reload 刷新配置文件

tasklist
/fi "imagename eq nginx.exe"查看所有的nginx进程

taskkill
/fi "imagename eq nginx.exe" /f 停止所有nginx进程

定位到解压的目录,然后在DOS窗口中执行  start nginx  启动nginx服务。

在使用前,我们需要检查nginx是否启动成功,直接在浏览器地址栏输入网址 http://localhost:80,回车,出现以下页面说明启动成功。

也可以在cmd命令窗口输入命令 tasklist /fi "imagename eq nginx.exe" ,出现如下结果说明启动成功

nginx的配置文件是conf目录下的nginx.conf,默认配置的nginx监听的端口为80,如果80端口被占用可以修改为未被占用的端口即可。

在处理网站的URL代理设置前,我们先回到我们Vue+Element 项目里面,我们在vue.config.js里面一般都有为跨域处理实现的代理设置,如下图所示。

而发布应用到服务器的时候,我们需要配置它的反向代理设置。

使用Nginx部署Vue+Element前端应用的时候,我们可以利用它的反向代理设置配置即可。

在nginx下的conf\nginx.conf中修改nginx的配置文件,配置修改。

根据我在Vue前端项目上的devServer的配置,我们在nginx的反向代理设置如下所示。

完整设置信息如下所示:

server {
listen
8000;
server_name localhost;

#charset koi8
-r;
#access_log logs
/host.access.log main; location/{
root html
/dist; index index.html index.htm;
try_files $uri $uri
/ /index.html =404; }
location
/baseabp { proxy_set_header Host $host:21021; #图片等资源需带端口获取
proxy_set_header x
-forwarded-for$remote_addr;
proxy_set_header X
-Real-IP $remote_addr;
proxy_pass http:
//localhost:21021; }
location
/abp { proxy_set_header Host $host:21021; #图片等资源需带端口获取
proxy_set_header x
-forwarded-for$remote_addr;
proxy_set_header X
-Real-IP $remote_addr;
proxy_pass http:
//localhost:21021/api; }

以上设置处理后,前端使用到Web API端的文件,反向代理也会带上对应的端口号,实现图片等上传API目录下的资源的正常访问了。

在很多列表展示数据的场合中,大多数都会需要一个排序的处理,以方便快速查找排序所需的数据,本篇随笔介绍如何结合ABP后端和Vue+Element前端结合的分页排序处理过程。

1、Vue+Element前端的排序代码

我们首先参考一下Element界面组件的排序说明。如下效果是Element表格组件的排序效果。

在列中设置
sortable
属性即可实现以该列为基准的排序,接受一个
Boolean
,默认为
false
。可以通过 Table 的
default-sort
属性设置默认的排序列和排序顺序。可以使用
sort-method
或者
sort-by
使用自定义的排序规则。
如果需要后端排序,需将
sortable
设置为
custom

,同时在 Table 上监听
sort-change
事件,在事件回调中可以获取当前排序的字段名和排序顺序,从而向接口请求排序后的表格数据。在本例中,我们还使用了
formatter
属性,它用于格式化指定列的值,接受一个
Function
,会传入两个参数:
row

column
,可以根据自己的需求进行处理。

根据上面说明,我们这里采取的是后端排序,将
sortable
设置为
custom。如下案例代码所示。

    <!--表格列表信息-->
    <el-tablev-loading="listLoading":data="list"border
fit
stripe
highlight-current-row
:header-cell-style
="{background:'#eef1f6',color:'#606266'}"@selection-change="selectionChange"@row-dblclick="rowDbclick"@sort-change="sortChange" > <el-table-columntype="selection"width="40" /> <el-table-columnalign="center"sortable="custom"prop="name"label="姓名"> <templateslot-scope="scope">{{ scope.row.name }}</template> </el-table-column> <el-table-columnalign="center"sortable="custom"prop="age"label="年龄"> <templateslot-scope="scope">{{ scope.row.age }} 岁</template> </el-table-column> <!--日期字段信息,显示完整时间格式:formatter="timeFormat",否则使用:formatter="dateFormat"--> <el-table-columnalign="center"label="创建时间"sortable="custom"width="160"prop="creationTime":formatter="timeFormat" />

以上是界面端的HTML代码,那么前端JS的处理逻辑也需要相应处理下,上面代码中 @sort-change="sortChange" 定义了排序变化的触发事件处理。

methods: {
sortChange({ column, prop, order }) {
var direction = (order === 'ascending') ? ' ASC' : ' DESC' this.sorting = !this.isEmpty(prop) ? (prop + direction) : null this.getlist()
},

以上代码主要逻辑是构建一个排序字符串,如:字段 ASC 或 字段 DESC的字符串,以供ABP后端的排序处理即可,这个主要是根据ABP框架排序特点进行提交的。

我们通过getlist的方法提交对应的查询条件和排序字符串,让后端再次获取数据给我们前端处理。

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

那么剩下的时候,就是后端如何处理数据的问题了。

2、 ABP后端分页排序的处理

我们查看ABP的应用服务层的接口基类可以看到,对应的获取所有记录GetAll方法的定义如下所示。

        public virtual async Task<PagedResultDto<TEntityDto>>GetAllAsync(TGetAllInput input)
{
CheckGetAllPermission();
//检查权限 var query = CreateFilteredQuery(input); //构建查询条件 var totalCount = await AsyncQueryableExecuter.CountAsync(query); //获取记录总数 query= ApplySorting(query, input); //排序 query = ApplyPaging(query, input); //分页 var entities = await AsyncQueryableExecuter.ToListAsync(query); //返回实体记录 return new PagedResultDto<TEntityDto>( //构建分页展示数据 totalCount,
entities.Select(MapToEntityDto).ToList() //实体转换DTO
);
}

一般情况下,这些逻辑我们一般不会变化,我们只需要进行一定的重写构建查询条件,以及排序规则即可。

例如重写条件查询获取查询对象的处理如下。

详细代码如下所示:

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Customer>CreateFilteredQuery(CustomerPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!input.ExcludeId.IsNullOrWhiteSpace(), t=>t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals//年龄区间查询 .WhereIf(input.AgeStart.HasValue, s => s.Age >=input.AgeStart.Value)
.WhereIf(input.AgeEnd.HasValue, s
=> s.Age <=input.AgeEnd.Value)//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value)
;
}

排序的处理,我们可以重写下规则如下所示。

        /// <summary>
        ///自定义排序处理/// </summary>
        /// <param name="query">可查询LINQ</param>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Customer> ApplySorting(IQueryable<Customer>query, CustomerPagedDto input)
{
if (!string.IsNullOrEmpty(input.Sorting))//排序字段是否有值 {
query
=query.OrderBy(input.Sorting);
}
else{
query
= query.OrderByDescending(t =>t.CreationTime);
}
returnquery; }

完成这些,我们就可以测试前端的界面效果了。

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

循序渐进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、系统登录日志

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

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变量方式解决弹出编辑对话框界面无法触发更新的问题