wenmo8 发布的文章

在前面随笔,我介绍了整个ABP优化过框架的分层模型,包括尽量简化整个ABP框架的各个层的关系,以及纳入一些基类的辅助处理,使得我们对应业务分层类或者接口尽可能减少代码,并具有生产环境所需要的基类接口,通过我对整个ABP框架模型的分析,我们可以结合代码生成工具Database2Sharp来生成对应分层的代码,该工具后台具备数据库表所需要的一切字段信息和关系信息,因此我们确定好逻辑关系就可以生成对应分层的代码。本篇随笔介绍代码生成工具Database2Sharp生成基于ABP框架的分层代码过程。

1)ABP框架回顾

ABP框架主要还是基于领域驱动的理念来构建整个架构的,其中领域驱动包含的概念有 域对象Entities、仓储对象Repositories、域服务接口层Domain Services、域事件Domain Events、应用服务接口Application Services、数据传输对象DTOs等。

以下是ABP初始框架的各个分层的信息,它主要是分为下面几个项目分层。

Application应用层
:应用层提供一些应用服务(Application Services)方法供展现层调用。一个应用服务方法接收一个DTO(数据传输对象)作为输入参数,使用这个输入参数执行特定的领域层操作,并根据需要可返回另一个DTO。

Core领域核心层
,领域层就是业务层,是一个项目的核心,所有业务规则都应该在领域层实现。这个项目里面,除了定义所需的领域实体类外,其实可以定义我们自己的自定义的仓储对象(类似DAL/IDAL),以及定义自己的业务逻辑层(类似BLL/IBLL),以及基于AutoMapper映射规则等内容。

EntityFrameworkCore
实体框架核心层,这个项目不需要修改太多内容,只需要在DbContext里面加入对应领域对象的仓储对象即可。

Migrator数据迁移层
,这个是一个辅助创建的控制台程序项目,如果基于DB First,我们可以利用它来创建我们项目的初始化数据库。

Web.Core Web核心层
,基于Web或者Web API的核心层,提供了对身份登陆验证的基础处理,没有其他内容。

Web.Core.Host Web API的宿主层
,也是动态发布Web API的核心内容,另外在Web API里面整合了Swagger,使得我们可以方便对Web API的接口进行调试。

Tests 单元测试层
,这个提供了一些应用层对象的模拟测试,其中测试的数据库使用的是Entity Framework 的内存数据库,不影响实际数据库内容。

经过我进行简化和优化处理的框架项目结构如下所示。

以上是VS里面解决方案的项目结构,我根据项目之间的关系,整理了一个架构的图形,如下所示。

上图是以字典模块为介绍, 其中橘红色的部分就是我们为各个分层需要根据数据库构建对应的类或者接口文件。

例如对于01-Core模块层,需要增加文件

对于03-Application.Common模块来说,需要增加DTO和应用服务层接口文件

而对于04-Application应用层来说,需要增加对应的接口实现文件

而05、06、07模块,我们不需要加入任何文件,08-Caller层加入对WebAPI的远程调用封装类,给Winform、WPF/UWP、控制台程序等调用。

一个模块的变化,都会导致在上面各个分层之间增加对应的文件,这样的架构确定后,我们就可以根据对应的类生成规则进行生成接口。

2)利用代码生成工具生成分层代码

在前面随笔《
代码生成工具Database2Sharp的架构介绍
》中,我介绍了整个代码生成工具的架构信息,因此我们用代码生成工具生成架构代码的时候,可以利用整个数据库表的信息和关系信息来处理。

通过整合相关的生成规则,我们可以增加对应的ABP框架代码的生成,如下代码生成工具界面所示。

最终根据根据选择数据库表信息,一键生成相关ABP架构分层代码,文件结构如下所示。

对比前面项目的介绍,我们可以看到各个分层的类代码是完全一致的。如对于领域层,包含了表名称标记、字段信息和引用外键的对象。

    /// <summary>
    ///通用字典明细项目信息,领域对象/// </summary>
    [Table("TB_DictData")]public class DictData : FullAuditedEntity<string>{/// <summary>
        ///默认构造函数(需要初始化属性的在此处理)/// </summary>
        publicDictData()
{
}
#region Property Members /// <summary> ///字典大类/// </summary> //[Required] public virtual string DictType_ID { get; set; }/// <summary> ///字典名称/// </summary> //[Required] public virtual string Name { get; set; }/// <summary> ///字典值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }/// <summary> ///字典大类/// </summary> [ForeignKey("DictType_ID")]public virtual DictType DictType { get; set; }#endregion}

对于DTO文件,我们看看代码信息

    /// <summary>
    ///通用字典明细项目信息,DTO对象/// </summary>
    public classDictDataDto
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicDictDataDto()
{
}
#region Property Members /// <summary> ///字典大类/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///字典名称/// </summary> public virtual string Name { get; set; }/// <summary> ///字典值/// </summary> //[Required] public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }#endregion}/// <summary> ///创建通用字典明细项目信息,DTO对象/// </summary> public classCreateDictDataDto : DictDataDto
{
}
/// <summary> ///用于根据条件分页查询,DTO对象/// </summary> public classDictDataPagedDto : PagedResultRequestDto
{
publicDictDataPagedDto() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="skipCount">跳过的数量</param> /// <param name="resultCount">最大结果集数量</param> public DictDataPagedDto(int skipCount, intresultCount)
{
this.SkipCount =skipCount;this.MaxResultCount =resultCount;
}
/// <summary> ///使用分页信息进行初始化SkipCount 和 MaxResultCount/// </summary> /// <param name="pagerInfo">分页信息</param> publicDictDataPagedDto(PagerInfo pagerInfo)
{
if (pagerInfo != null)
{
//默认设置 var pageSize = pagerInfo.PageSize > 0 ? pagerInfo.PageSize : 50;var pageIndex = pagerInfo.CurrenetPageIndex > 0 ? pagerInfo.CurrenetPageIndex : 1;this.SkipCount = pageSize * (pageIndex - 1);this.MaxResultCount =pageSize;
}
}
#region Property Members /// <summary> ///字典大类/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///字典名称/// </summary> public virtual string Name { get; set; }/// <summary> ///字典值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }#endregion}

DTO的映射文件代码生成如下

    /// <summary>
    ///通用字典明细项目信息,映射文件/// </summary>
    public classDictDataMapProfile : Profile  
{
publicDictDataMapProfile()
{
CreateMap
<DictDataDto, DictData>();
CreateMap
<DictData, DictDataDto>();
CreateMap
<CreateDictDataDto, DictData>();
}
}

应用服务层接口实现代码如下所示。

    /// <summary>
    ///通用字典明细项目信息,应用层服务接口实现/// </summary>
[AbpAuthorize]public class DictDataAppService : MyAsyncServiceBase<DictData, DictDataDto, string, DictDataPagedDto, CreateDictDataDto, DictDataDto>, IDictDataAppService
{
private readonly IRepository<DictData, string>_repository;public DictDataAppService(IRepository<DictData, string> repository) : base(repository)
{
_repository
=repository;
}
/// <summary> ///自定义条件处理/// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override IQueryable<DictData>CreateFilteredQuery(DictDataPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!DictType_ID.IsNullOrWhiteSpace(), t =>t.DictType_ID.Contains(input.DictType_ID))
.WhereIf(
!Name.IsNullOrWhiteSpace(), t =>t.Name.Contains(input.Name))
.WhereIf(
!Value.IsNullOrWhiteSpace(), t =>t.Value.Contains(input.Value))
.WhereIf(
!Remark.IsNullOrWhiteSpace(), t =>t.Remark.Contains(input.Remark))
.WhereIf(
!Seq.IsNullOrWhiteSpace(), t =>t.Seq.Contains(input.Seq));
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override IQueryable<DictData> ApplySorting(IQueryable<DictData>query, DictDataPagedDto input)
{
return base.ApplySorting(query, input);//示例代码//先按字典类型排序,然后同一个字典类型下的再按Seq排序//return base.ApplySorting(query, input).OrderBy(s=>s.DictType_ID).ThenBy(s => s.Seq); }
}

ApiCaller分层的代码实现如下所示。

    /// <summary>
    ///通用字典明细项目信息的Web API调用处理/// </summary>
    public class DictDataApiCaller : AsyncCrudApiCaller<DictDataDto, string, DictDataPagedDto, CreateDictDataDto, DictDataDto>, IDictDataAppService
{
/// <summary> ///提供单件对象使用/// </summary> public staticDictDataApiCaller Instance
{
get{return Singleton<DictDataApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicDictDataApiCaller()
{
this.DomainName = "DictData";//指定域对象名称,用于组装接口地址 }

}

这些信息是根据数据库对应字段信息和关系信息进行批量生成,我们可以在这基础上进行一定的调整,以及增加自己的业务接口,那么就非常方便了。

利用代码生成工具的数据库元数据,结合模板引擎NVelocity,我们可以为我们的项目框架代码快速生成提供了一个快速有效、统一标准的生成方式,大大提高了生产效率。

最近没有更新ABP框架的相关文章,一直在研究和封装相关的接口,总算告一段落,开始继续整理下开发心得。上次我在随笔《
ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用
》中介绍了字典模块的管理,以及实现了常规的获取所有记录,获取条件查询记录,创建、更新、删除这些接口。本篇继续深入介绍ABP框架在实际项目中使用的情况,本篇随笔整理对ABP基础接口,以及展示完成的省份城市行政区管理模块的内容。

1、ABP常规处理接口

根据ABP框架默认提供的一些接口,我们可以在服务端封装好相关的Web API接口(由于动态API的便利,其实是完成ApplicationService层即可),前面介绍了获取条件查询记录,创建、更新、删除这些接口的实现和处理,以及可以扩展自己的自定义业务接口,如下是字典模块的接口关系。

字典管理界面,列出字典类型,并对字典类型下的字典数据进行分页展示,分页展示利用分页控件展示。

新增或者编辑窗体界面如下

或者是批量的字典数据录入

这个精确或者模糊查询,则是在应用服务层里面定义规则的,在应用服务层接口类里面,重写CreateFilteredQuery可以设置GetAll的查询规则,重写ApplySorting则可以指定列表的排序顺序。

2、ABP常规查询接口的细化

在前面介绍了的内容汇总,基本上实现了常规数据的分页查询,我们可以看到,对于字典数据来说,分页查询条件是在DictDataPagedDto里面定义,这个是我们定义的分页条件,如下代码所示。

    /// <summary>
    ///用于根据条件分页查询/// </summary>
    public classDictDataPagedDto : PagedResultRequestDto
{
/// <summary> ///字典类型ID/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///类型名称/// </summary> public virtual string Name { get; set; }/// <summary> ///指定值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }
}

这个类文件,我们一般把这个业务模块相关的统一放在一个文件中,例如字典数据相关的DTO放在一个DictDataDto文件里面,方便管理,如下所示。

上面是字典模块的一些基础介绍,实际上我们开发业务模块的时候,录入数据的时候,还需要一个判断的步骤,如不允许名称重复的情况。在创建新的记录和更新已有记录都需要进行必要的判断,保证数据的有效性和不重复性。

如对于省份管理界面来说,我们不能运行重复录入省份名称,那么就需要在录入数据或者更新数据的时候,进行必要的存在性判断。

那么上面的处理是如何实现的呢。

主要的界面实现代码如下所示。

if (string.IsNullOrEmpty(ID))
{
//判断存在条件 var countDto = new ProvincePagedDto() { ProvinceName = this.txtProvince.Text };bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;if(isExist)
{
MessageDxUtil.ShowTips(
"省份名称已存在,请选择其他名称");this.txtProvince.Focus();return;
}
else{//创建新记录 tempInfo = awaitProvinceApiCaller.Instance.Create(tempInfo);
}
}
else{//判断存在条件,排除本记录同名情况 var countDto = new ProvincePagedDto() { ProvinceName = this.txtProvince.Text, ExcludeId =ID.ToInt64() };bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;if(isExist)
{
MessageDxUtil.ShowTips(
"省份名称已存在,请选择其他名称");this.txtProvince.Focus();return;
}
else{//更新记录 tempInfo = awaitProvinceApiCaller.Instance.Update(tempInfo);
}
}

ProcessDataSaved(
this.btnOK, newEventArgs());this.DialogResult = System.Windows.Forms.DialogResult.OK;

我们发现,这里增加了一个Count的函数用来判断,传入的条件就是前面的分页请求条件。

bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;

我们看看我们的应用服务层的接口实现如下所示。

        /// <summary>
        ///获取指定条件的数量/// </summary>
        /// <param name="input">查找条件</param>
        /// <returns></returns>
        public async virtual Task<int>Count(TGetAllInput input)
{
var query =CreateFilteredQuery(input);return awaitTask.FromResult(query.Count());
}

这里最终还是跳转到 CreateFilteredQuery 函数里面实现判断逻辑了。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Province>CreateFilteredQuery(ProvincePagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(input.ExcludeId.HasValue, t
=>t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.ProvinceName.IsNullOrWhiteSpace(), t =>t.ProvinceName.Contains(input.ProvinceName));
}

这里面包含了两个判断条件,一个是排除指定的ID记录,一个是匹配省份名称。

因为我们在更新记录的时候,需要判断非本记录是否有重复的名称。

//判断存在条件,排除本记录同名情况
var countDto = new ProvincePagedDto() { ProvinceName = this.txtProvince.Text, ExcludeId =ID.ToInt64() };bool isExist = await ProvinceApiCaller.Instance.Count(countDto) > 0;

这个ExcludeId 我们在分页条件里面增加一个固定的属性即可。

以上的分页信息,包含了实体DTO对象的一些属性,我们可以根据需要增加或者减少一部分属性。

另外我们定义的创建省份Dto对象和获取到单个实体的DTO对象,他们的定义和关系如下所示,方便我们在界面上进行操作。

    /// <summary>
    ///创建全国省份表,DTO对象/// </summary>
    public class CreateProvinceDto : EntityDto<long>{/// <summary>
        ///默认构造函数(需要初始化属性的在此处理)/// </summary>
        publicCreateProvinceDto()
{
}
#region Property Members /// <summary> ///省份名称/// </summary> [Required]public virtual string ProvinceName { get; set; }#endregion}/// <summary> ///全国省份表,DTO对象/// </summary> public classProvinceDto : CreateProvinceDto
{

}

固定这些规则后,我们也可以用代码生成工具快速生成对应的DTO文件了。

有了这些分页属性后,我们就可以在应用服务层里面定义自己的过滤规则了,如对于字典类型的应用服务层的筛选条件函数,如下所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        protected override IQueryable<DictType>CreateFilteredQuery(DictTypePagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!string.IsNullOrEmpty(input.ExcludeId), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!string.IsNullOrEmpty(input.Name), t =>t.Name.Contains(input.Name))
.WhereIf(
!string.IsNullOrEmpty(input.Remark), t =>t.Remark.Contains(input.Remark))
.WhereIf(
!string.IsNullOrEmpty(input.Code), t => t.Code ==input.Code)
.WhereIf(
!string.IsNullOrEmpty(input.PID), t => t.PID ==input.PID);
}

上面是对于包含、相等或者不等于的三种情况的条件判断,如果我们还需要一个时间区间范围或者数值范围的判断,那么同样可以在这里进行管理规则,如下是针对产品应用服务层的过滤规则,如下代码所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Product>CreateFilteredQuery(ProductPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.ProductNo.IsNullOrWhiteSpace(), t => t.ProductNo.Contains(input.ProductNo)) //如需要精确匹配则用Equals .WhereIf(!input.BarCode.IsNullOrWhiteSpace(), t => t.BarCode.Contains(input.BarCode)) //如需要精确匹配则用Equals .WhereIf(!input.MaterialCode.IsNullOrWhiteSpace(), t => t.MaterialCode.Contains(input.MaterialCode)) //如需要精确匹配则用Equals .WhereIf(!input.ProductType.IsNullOrWhiteSpace(), t => t.ProductType.Contains(input.ProductType)) //如需要精确匹配则用Equals .WhereIf(!input.ProductName.IsNullOrWhiteSpace(), t => t.ProductName.Contains(input.ProductName)) //如需要精确匹配则用Equals .WhereIf(!input.Unit.IsNullOrWhiteSpace(), t => t.Unit.Contains(input.Unit)) //如需要精确匹配则用Equals .WhereIf(!input.Note.IsNullOrWhiteSpace(), t => t.Note.Contains(input.Note)) //如需要精确匹配则用Equals .WhereIf(!input.Description.IsNullOrWhiteSpace(), t => t.Description.Contains(input.Description)) //如需要精确匹配则用Equals//状态 .WhereIf(input.Status.HasValue, t => t.Status==input.Status)//成本价区间查询 .WhereIf(input.PriceStart.HasValue, s => s.Price >=input.PriceStart.Value)
.WhereIf(input.PriceEnd.HasValue, s
=> s.Price <=input.PriceEnd.Value)//销售价区间查询 .WhereIf(input.SalePriceStart.HasValue, s => s.SalePrice >=input.SalePriceStart.Value)
.WhereIf(input.SalePriceEnd.HasValue, s
=> s.SalePrice <=input.SalePriceEnd.Value)//特价区间查询 .WhereIf(input.SpecialPriceStart.HasValue, s => s.SpecialPrice >=input.SpecialPriceStart.Value)
.WhereIf(input.SpecialPriceEnd.HasValue, s
=> s.SpecialPrice <=input.SpecialPriceEnd.Value)
.WhereIf(input.IsUseSpecial.HasValue, t
=> t.IsUseSpecial == input.IsUseSpecial) //如需要精确匹配则用Equals//最低折扣区间查询 .WhereIf(input.LowestDiscountStart.HasValue, s => s.LowestDiscount >=input.LowestDiscountStart.Value)
.WhereIf(input.LowestDiscountEnd.HasValue, s
=> s.LowestDiscount <=input.LowestDiscountEnd.Value)//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}

以上就是我们深入对分页查询和判断是否存在接口的细节处理,可以包含很多自定义的条件,如等于或不等于、包含或者不包含,区间查询(大于或者小于等)条件的处理。对于省份城市行政区管理模块的重复性判断,我们通过Count函数来判断,同时在后台应用服务层对这些参数进行规则过滤即可。

我们了解ABP框架内部自动记录审计日志和登录日志的,但是这些信息只是在相关的内部接口里面进行记录,并没有一个管理界面供我们了解,但是其系统数据库记录了这些数据信息,我们可以为它们设计一个查看和导出这些审计日志和登录日志的管理界面。本篇随笔继续ABP框架的系列介绍,一步步深入了解ABP框架的应用开发,介绍审计日志和登录日志的管理。

1、审计日志和登录日志的基础

审计日志,设置我们在访问或者调用某个应用服务层接口的时候,横切面流下的一系列操作记录,其中记录我们访问的服务接口,参数,客户端IP地址,访问时间,以及异常等信息,这些操作都是在ABP系统自动记录的,如果我们需要屏蔽某些服务类或者接口,则这些就不会记录在里面,否则默认是记录的。

登录日志,这个就是用户尝试登录的时候,留下的记录信息,其中包括用户的登录用户名,ID,IP地址、登录时间,以及登录是否成功的状态等信息。

我们查看系统数据库,可以看到对应这两个部分的日志表,如下所示。

在ABP框架内部基础项目Abp里面,我们可以看到对应的领域对象实体和Store管理类,不过并没有在应用层的对应服务和相关的DTO,我们需要实现一个审计日志和登陆日志的管理功能界面,界面效果如下所示。

我们搜索ABP项目,查找到审计日志的相关类(包含领域对象实体和Store管理类),如下界面截图。

同样对于系统登录日志对象,我们查找到对应的领域实体和对应的Manger业务逻辑类。

这些也就代表它们都有底层的实现,但是没有服务层应用和DTO对象,因此我们需要扩展这些内容才能够管理显示这些记录信息。

前面介绍过,默认的一般应用服务层和接口,都是会进行审计记录写入的,如果我们需要屏蔽某些应用服务层或者接口,不进行审计信息的记录,那么需要使用特性标记[DisableAuditing]来管理。

如我们针对审计日志应用层接口的访问,我们不想让它多余的记录,那么就设置这个标记即可。

或者屏蔽某些接口

另外,如果我们不想公布某些特殊的接口访问,那么我们可以通过标记 [RemoteService(false)]  进行屏蔽,这样在Web API层就不会公布对应的接口了。

如对于审计日志的记录,增删改我们都不允许客户端进行操作,那么我们把对应的应用服务层接口屏蔽即可。

2、系统审计日志和登录日志的完善

前面介绍了,审计日志和登陆日志的处理,Abp系统只是做了一部分底层的内容,我们如果进行这些信息的管理,我们需要完善它,增加对应的DTO类和应用服务层接口和接口实现。

首先我们根据底层的领域实体对象的属性,复制过来作为对应DTO对象的属性,并增加对应的分页条件DTO对象,由于我们不需要进行创建,因此不需要增加Create***Dto对象类。

如对于审计日志的DTO对象,我们定义如下所示(主要复制领域对象的属性)。

而分页处理的DTO对象如下所示,我们主要增加一个用户名和创建时间区间的条件。

对于登录日志的DTO对象,我们依葫芦画瓢,也是如此操作即可。

登录日志的分页对象Dto如下所示、

完善了这些DTO对象,下一步我们需要创建对应的应用服务层类,这样我们才能在客户端通过Web API获取对应的数据。

首先我们来定义审计日志应用服务类,如下所示。

    [DisableAuditing] //屏蔽这个AppService的审计功能
[AbpAuthorize]public class AuditLogAppService : AsyncCrudAppService<AuditLog, AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto>{private readonly IRepository<AuditLog, long>_repository;private readonlyIAuditingStore _stroe;private readonly IRepository<User, long>_userRepository;public AuditLogAppService(IRepository<AuditLog, long> repository, IAuditingStore stroe, IRepository<User, long> userRepository) : base(repository)
{
_repository
=repository;
_stroe
=stroe;
_userRepository
=userRepository;
}

......

其中我们需要IRepository<User, long>用来转义用户ID为对应的用户名,这样对于我们显示有帮助。

默认来说,这个应用服务层已经具有常规的增删改查、分页等基础接口了,但是我们不需要对外公布增删改接口,我们需要重写实现把它屏蔽。

        /// <summary>
        ///屏蔽创建接口/// </summary>
        [RemoteService(false)]public override Task<AuditLogDto>Create(AuditLogDto input)
{
return base.Create(input);
}
/// <summary> ///屏蔽更新接口/// </summary> [RemoteService(false)]public override Task<AuditLogDto>Update(AuditLogDto input)
{
return base.Update(input);
}
/// <summary> ///屏蔽删除接口/// </summary> [RemoteService(false)]public override Task Delete(EntityDto<long>input)
{
return base.Delete(input);
}

那么我们就剩下GetAll和Get两个方法了,我们如果不需要转义特殊内容,我们就可以不重写它,但是我们这里需要对用户ID转义为用户名称,那么需要进行一个处理,如下所示。

[DisableAuditing]public override Task<PagedResultDto<AuditLogDto>>GetAll(AuditLogPagedDto input)
{
var result = base.GetAll(input);foreach (var item inresult.Result.Items)
{
ConvertDto(item);
//对用户名称进行解析 }returnresult;
}
[DisableAuditing]
public override Task<AuditLogDto> Get(EntityDto<long>input)
{
var result = base.Get(input);
ConvertDto(result.Result);
returnresult;
}
/// <summary> ///对记录进行转义/// </summary> /// <param name="item">dto数据对象</param> /// <returns></returns> protected virtual voidConvertDto(AuditLogDto item)
{
//用户名称转义 if(item.UserId.HasValue)
{
item.UserName
=_userRepository.Get(item.UserId.Value).UserName;
}
//IP地址转义 if (!string.IsNullOrEmpty(item.ClientIpAddress))
{
item.ClientIpAddress
= item.ClientIpAddress.Replace("::1", "127.0.0.1");
}
}

这里主要就用户ID和IP地址进行一个正常的转义处理,这个也是我们常规接口需要处理的一种常见的情况之一。

排序我们是以执行时间进行排序,倒序显示即可,因此重写排序函数。

        /// <summary>
        ///自定义排序处理/// </summary>
        /// <param name="query"></param>
        /// <param name="input"></param>
        /// <returns></returns>
        protected override IQueryable<AuditLog> ApplySorting(IQueryable<AuditLog>query, AuditLogPagedDto input)
{
return base.ApplySorting(query, input).OrderByDescending(s => s.ExecutionTime);//时间降序 }

一般情况下,我们就基本完成了这个模块的处理了,这样我们在界面上在花点功夫就可以调用这个API接口进行显示信息了,如下界面是我编写的审计日志分页列表显示界面。

明细展示界面如下所示。

上面列表界面管理中,如果我们还能够以用户进行过滤,那就更好了,因此需要添加一个用户名进行过滤(注意不是用户ID),系统表里面没有用户名称。

如果我们需要用户名称过滤,如下界面所示。

那么我们就需要在应用服务层的过滤函数里面处理相应的规则了。

我们先创建一个审计日志和用户信息的集合对象,如下所示。

    /// <summary>
    ///审计日志和用户的领域对象集合/// </summary>
    public classAuditLogAndUser
{
public AuditLog AuditLog { get;set;}public User User { get; set; }
}

然后在 CreateFilteredQuery 函数里面进行处理,如下代码所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">分页查询Dto对象</param>
        /// <returns></returns>
        protected override IQueryable<AuditLog>CreateFilteredQuery(AuditLogPagedDto input)
{
//构建关联查询Query var query = from auditLog inRepository.GetAll()
join user
in_userRepository.GetAll() on auditLog.UserId equals user.Id into userJoinfrom joinedUser inuserJoin.DefaultIfEmpty()whereauditLog.UserId.HasValueselect new AuditLogAndUser { AuditLog = auditLog, User =joinedUser };//过滤分页条件 returnquery
.WhereIf(
!string.IsNullOrEmpty(input.UserName), t =>t.User.UserName.Contains(input.UserName))
.WhereIf(input.ExecutionTimeStart.HasValue, s
=> s.AuditLog.ExecutionTime >=input.ExecutionTimeStart.Value)
.WhereIf(input.ExecutionTimeEnd.HasValue, s
=> s.AuditLog.ExecutionTime <=input.ExecutionTimeEnd.Value)
.Select(s
=>s.AuditLog);
}

上面其实就是先通过EF的关联表查询,返回一个集合记录,然后在判断用户名是否在集合里面,最后返回所需的实体对象列表。

这个EF的关联表查询非常关键,这个也是我们联合查询的精髓所在,通过LINQ的方式,可以很方便实现关联表的查询处理并获得对应的结果。

而对于用户登录日志,由于系统记录了用户名,那么过滤用户名,这不需要这么大费周章关联表进行处理,只需要判断数据库字段对应情况即可,这种方便很多。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        protected override IQueryable<UserLoginAttempt>CreateFilteredQuery(UserLoginAttemptPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!string.IsNullOrEmpty(input.UserNameOrEmailAddress), t =>t.UserNameOrEmailAddress.Contains(input.UserNameOrEmailAddress))
.WhereIf(input.CreationTimeStart.HasValue, s
=> s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}

同样系统用户登录日志界面如下所示。

用户登录明细界面效果如下所示。

以上就是对于审计日志和用户登录日志的扩展实现,包括了对相关DTO的增加和实现应用服务层接口,以及对Web API Caller层的实现。

    /// <summary>
    ///审计日志的Web API调用处理/// </summary>
    public class AuditLogApiCaller : AsyncCrudApiCaller<AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto>{/// <summary>
        ///提供单件对象使用/// </summary>
        public staticAuditLogApiCaller Instance
{
get{return Singleton<AuditLogApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicAuditLogApiCaller()
{
this.DomainName = "AuditLog";//指定域对象名称,用于组装接口地址 }
}

由于只是部分实现功能,我们还是可以基于前面介绍开发模式(利用代码生成工具Database2Sharp快速生成)来实现ABP优化框架类文件的生成,以及界面代码的生成,然后进行一定的调整就是本项目的代码了。

代码生成工具的ABP项目代码模板,和基于ABPWinform界面代码的模板,是我基于实际项目的反复优化和验证,并尽量减少冗余代码而完成的一种快速开发方式,基于这样开发方式可以大大减少项目开发的难度,提高开发效率,并完全匹配整个框架的需要,是一种非常惬意的快速开发方式。

在前面随笔介绍的《
ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理
》里面,介绍了如何改进和完善审计日志和登录日志的应用服务端和Winform客户端,由于篇幅限制,没有进一步详细介绍Winform界面的开发过程,本篇随笔介绍这部分内容,并进一步扩展Winform界面的各种情况处理,力求让它进入一个新的开发里程碑。

1、回顾审计日志和登陆日志管理界面

前面介绍了如何扩展审计日志应用服务层(Application Service层)和ApiCaller层(API客户端调用封装层),同时也展示审计日志和登录日志在Winform界面的展示,由于整个ABP框架目前我还是采用了.net core的开发路线,所有的封装项目都是基于.net core基础上进行的。不过由于目前Winform还没有能够以 .net core进行开发,所以界面端还是用.net framework的方式开发,不过可以调用 .net standard的类库。

下面是审计日志的列表展示界面,和我之前的Winform框架一样的布局,因此我重用了Winform框架里面公用类库项目、基础界面封装项目、分页控件等内容,因此整个界面看起来还是很一致的。

由于审计日志主要供底层记录,因此在界面不能增加增删改的操作,我们只需要分页查询,和导出记录即可,如下窗体界面所示。

而明细内容,可以通过双击或者右键选择菜单打开即可弹出新的展示界面,主要展示审计日志里面的各项信息。

而对于用户登录日志来说,处理方式差不多,也是通过在列表中查询展示,并在列表中整合右键菜单或者双击处理,可以查看登录明细内容。

通过双击或者右键选择菜单打开即可弹出新的展示界面,主要展示登录日志里面的各项信息。

2、Winform界面代码实现

上面展示了列表界面和查看明细界面,实际上我们Winform的界面内部是如何处理的呢,我们这里对其中的一些关键处理进行分析介绍。

列表界面的窗体初始化代码如下所示

    /// <summary>
    ///审计日志/// </summary>    
    public partial classFrmAuditLog : BaseDock
{
private const string Id_FieldName = "Id";//Id的字段名称 publicFrmAuditLog()
{
InitializeComponent();
//分页控件初始化事件 this.winGridViewPager1.OnPageChanged += newEventHandler(winGridViewPager1_OnPageChanged);this.winGridViewPager1.OnStartExport += newEventHandler(winGridViewPager1_OnStartExport);this.winGridViewPager1.OnEditSelected += newEventHandler(winGridViewPager1_OnEditSelected);this.winGridViewPager1.OnAddNew += newEventHandler(winGridViewPager1_OnAddNew);this.winGridViewPager1.OnDeleteSelected += newEventHandler(winGridViewPager1_OnDeleteSelected);this.winGridViewPager1.OnRefresh += newEventHandler(winGridViewPager1_OnRefresh);this.winGridViewPager1.AppendedMenu = this.contextMenuStrip1;this.winGridViewPager1.ShowLineNumber = true;this.winGridViewPager1.BestFitColumnWith = false;//是否设置为自动调整宽度,false为不设置 this.winGridViewPager1.gridView1.DataSourceChanged +=newEventHandler(gridView1_DataSourceChanged);this.winGridViewPager1.gridView1.CustomColumnDisplayText += newDevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventHandler(gridView1_CustomColumnDisplayText);this.winGridViewPager1.gridView1.RowCellStyle += newDevExpress.XtraGrid.Views.Grid.RowCellStyleEventHandler(gridView1_RowCellStyle);//关联回车键进行查询 foreach (Control control in this.layoutControl1.Controls)
{
control.KeyUp
+= new System.Windows.Forms.KeyEventHandler(this.SearchControl_KeyUp);
}
//屏蔽某些处理 this.winGridViewPager1.ShowAddMenu = false;this.winGridViewPager1.ShowDeleteMenu = false;
}

这些是使用分页控件来初始化一些界面的处理事件,不要一看就抱怨需要编写这么多代码,这些基本上都是代码生成工具生成的,后面会介绍。

其实窗体的加载的时候,主要逻辑是初始化字典列表和展示列表数据,如下代码所示。

        /// <summary>
        ///编写初始化窗体的实现,可以用于刷新/// </summary>
        public override async voidFormOnLoad()
{
awaitInitDictItem();awaitBindData();
}

其中这里都是使用async和await 配对实现的异步处理操作。我们对于审计日志列表来说,字典模块没有需要字典绑定信息,那么默认为空不用修改。

        /// <summary>
        ///初始化字典列表内容/// </summary>
        private asyncTask InitDictItem()
{
//初始化代码//await this.txtCategory.BindDictItems("报销类型"); await Task.FromResult(0);
}

那么我们主要处理的就是BindData的数据绑定操作了。

        /// <summary>
        ///绑定列表数据/// </summary>
        private asyncTask BindData()
{
this.winGridViewPager1.DisplayColumns = "Id,BrowserInfo,ClientIpAddress,ClientName,CreationTime,Result,UserId,UserNameOrEmailAddress";this.winGridViewPager1.ColumnNameAlias = await UserLoginAttemptApiCaller.Instance.GetColumnNameAlias();//字段列显示名称转义//获取分页数据列表 var result = awaitGetData();//设置所有记录数和列表数据源 this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount; //需先于DataSource的赋值,更新分页信息 this.winGridViewPager1.DataSource =result.Items;this.winGridViewPager1.PrintTitle = "用户登录日志报表";
}

其中我们通过 调用服务端接口 GetColumnNameAlias 来获取对应的别名,其实我们也可以在Winform客户端设置对等的别名处理,如下代码所示。

            #region 添加别名解析

            //this.winGridViewPager1.AddColumnAlias("Id", "Id");//this.winGridViewPager1.AddColumnAlias("BrowserInfo", "浏览器");//this.winGridViewPager1.AddColumnAlias("ClientIpAddress", "IP地址");//this.winGridViewPager1.AddColumnAlias("ClientName", "客户端");//this.winGridViewPager1.AddColumnAlias("CreationTime", "时间");//this.winGridViewPager1.AddColumnAlias("Result", "结果");//this.winGridViewPager1.AddColumnAlias("UserId", "用户ID");//this.winGridViewPager1.AddColumnAlias("UserNameOrEmailAddress", "用户名或邮件");

            #endregion

只是基于服务端更加方便,也减少客户端的编码了。

而获取数据主要通过 GetData 函数进行统一获取对应的列表和数据记录信息,如下是GetData的函数实现。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<UserLoginAttemptDto>>GetData()
{
//构建分页的条件和查询条件 var pagerDto = new UserLoginAttemptPagedDto(this.winGridViewPager1.PagerInfo)
{
UserNameOrEmailAddress = this.txtUserNameOrEmailAddress.Text.Trim(),
};
//日期和数值范围定义//时间,需在UserLoginAttemptPagedDto中添加DateTime?类型字段CreationTimeStart和CreationTimeEnd var CreationTime = new TimeRange(this.txtCreationTime1.Text, this.txtCreationTime2.Text); //日期类型 pagerDto.CreationTimeStart =CreationTime.Start;
pagerDto.CreationTimeEnd
=CreationTime.End;var result = awaitUserLoginAttemptApiCaller.Instance.GetAll(pagerDto);returnresult;
}

这个函数里面,主要是接收列表界面里面的查询条件,并构建对应的分页查询条件,这样根据条件DTO就可以请求服务器的数据了。

前面讲了,这个过滤条件并返回对应的数据,主要就是在Application Service层,设置CreateFilteredQuery的控制逻辑即可,如下所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">分页查询Dto对象</param>
        /// <returns></returns>
        protected override IQueryable<AuditLog>CreateFilteredQuery(AuditLogPagedDto input)
{
//构建关联查询Query var query = from auditLog inRepository.GetAll()
join user
in_userRepository.GetAll() on auditLog.UserId equals user.Id into userJoinfrom joinedUser inuserJoin.DefaultIfEmpty()whereauditLog.UserId.HasValueselect new AuditLogAndUser { AuditLog = auditLog, User =joinedUser };//过滤分页条件 returnquery
.WhereIf(
!string.IsNullOrEmpty(input.UserName), t =>t.User.UserName.Contains(input.UserName))
.WhereIf(input.ExecutionTimeStart.HasValue, s
=> s.AuditLog.ExecutionTime >=input.ExecutionTimeStart.Value)
.WhereIf(input.ExecutionTimeEnd.HasValue, s
=> s.AuditLog.ExecutionTime <=input.ExecutionTimeEnd.Value)
.Select(s
=>s.AuditLog);
}

这里就不在赘述服务层的逻辑代码,主要关注我们本篇的主题,Winform的界面实现逻辑。

上面通过GetData获取到服务端数据后,我们就可以把列表数据绑定到分页控件上面,让分页控件调用GridControl 进行展示出来即可。

            //设置所有记录数和列表数据源
            this.winGridViewPager1.PagerInfo.RecordCount =result.TotalCount;this.winGridViewPager1.DataSource = result.Items;

数据的导出操作,我们这里也顺便提一下,虽然这些代码是基于代码生成工具生成的,不过还是提一下逻辑处理。

数据的导出操作,主要就是通过GetData获取到数据后,转换为DataTable,并通过Apose.Cell进行写入Excel文件即可,如下代码所示。

        /// <summary>
        ///导出的操作/// </summary>        
        private async voidExportData()
{
string file = FileDialogHelper.SaveExcel(string.Format("{0}.xls", moduleName));if (!string.IsNullOrEmpty(file))
{
//获取分页数据列表 var result = awaitGetData();var list =result.Items;
DataTable dtNew
= DataTableHelper.CreateTable("序号|int,Id,时间,用户名,服务,操作,参数,持续时间,IP地址,客户端,浏览器,自定义数据,异常,返回值");
DataRow dr;
int j = 1;for (int i = 0; i < list.Count; i++)
{
dr
=dtNew.NewRow();
dr[
"序号"] = j++;
dr[
"Id"] =list[i].Id;
dr[
"浏览器"] =list[i].BrowserInfo;
dr[
"IP地址"] =list[i].ClientIpAddress;
dr[
"客户端"] =list[i].ClientName;
dr[
"自定义数据"] =list[i].CustomData;
dr[
"异常"] =list[i].Exception;
dr[
"持续时间"] =list[i].ExecutionDuration;
dr[
"时间"] =list[i].ExecutionTime;
dr[
"操作"] =list[i].MethodName;
dr[
"参数"] =list[i].Parameters;
dr[
"服务"] =list[i].ServiceName;
dr[
"用户名"] =list[i].UserName;
dr[
"返回值"] =list[i].ReturnValue;
dtNew.Rows.Add(dr);
}
try{string error = "";
AsposeExcelTools.DataTableToExcel2(dtNew, file,
outerror);if (!string.IsNullOrEmpty(error))
{
MessageDxUtil.ShowError(
string.Format("导出Excel出现错误:{0}", error));
}
else{if (MessageDxUtil.ShowYesNoAndTips("导出成功,是否打开文件?") ==System.Windows.Forms.DialogResult.Yes)
{
System.Diagnostics.Process.Start(file);
}
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
}

而对于编辑或者查看界面,如下所示。

它的实现逻辑主要就是获取单个记录,然后在界面上逐一绑定控件内容显示即可。

        /// <summary>
        ///数据显示的函数/// </summary>
        public async override voidDisplayData()
{
InitDictItem();
//数据字典加载(公用) if (!string.IsNullOrEmpty(ID))
{
#region 显示信息 var info = awaitAuditLogApiCaller.Instance.Get(ID.ToInt64());if (info != null)
{
tempInfo
= info;//重新给临时对象赋值,使之指向存在的记录对象 txtBrowserInfo.Text=info.BrowserInfo;
txtClientIpAddress.Text
=info.ClientIpAddress;
txtClientName.Text
=info.ClientName;
txtCustomData.Text
=info.CustomData;
txtException.Text
=info.Exception;
txtExecutionDuration.Value
=info.ExecutionDuration;
txtExecutionTime.SetDateTime(info.ExecutionTime);
txtMethodName.Text
=info.MethodName;
txtParameters.Text
=ConvertJson(info.Parameters);
txtServiceName.Text
=info.ServiceName;if(info.UserId.HasValue)
{
txtUserId.Value
=info.UserId.Value;
}
txtUserName.Text
= info.UserName;//转义的用户名 }#endregion}else{
}
this.btnAdd.Visible = false;this.btnOK.Visible = false;
}

当然对于新增或编辑的界面,我们需要处理它的保存或者更新的操作事件,虽然审计日志不需要这些操作,不过生成的编辑窗体界面,依旧保留这些处理逻辑,如下代码所示。

        /// <summary>
        ///新增状态下的数据保存/// </summary>
        /// <returns></returns>
        public async override Task<bool>SaveAddNew()
{
AuditLogDto info
= tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用 SetInfo(info);try{#region 新增数据tempInfo= awaitAuditLogApiCaller.Instance.Create(info);if (tempInfo != null)
{
//可添加其他关联操作 return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
return false;
}
/// <summary> ///编辑状态下的数据保存/// </summary> /// <returns></returns> public async override Task<bool>SaveUpdated()
{
AuditLogDto info
= awaitAuditLogApiCaller.Instance.Get(ID.ToInt64());if (info != null)
{
SetInfo(info);
try{#region 更新数据tempInfo= awaitAuditLogApiCaller.Instance.Update(info);if (tempInfo != null)
{
//可添加其他关联操作 return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
return false;
}

我们可以根据实际的需要,对我们业务对象的窗体进行一定的改造即可。

3、复杂一点的WInform界面处理

例如对于前面的列表界面,一个比较复杂一点的列表展示内容,需要在查询条件中绑定字典列表,并对列表记录的一些状态进行特殊展示等,以及需要考虑增加、导入、导出等功能按钮,这些默认的列表生成界面就有的。

如下是对于产品信息的一个界面展示,也是基于ABP框架构建的服务进行数据展示的例子。

和前面介绍的例子一样,也是基于分页控件进行展示的,我们来看看状态的处理吧。

由于状态和用户信息,我们在数据库里面记录的是整形的数据信息,也就是状态为0,1的这样,以及用户ID等,我们如果需要转义给客户端使用,那么我们需要在对应的DTO里面增加一些字段进行承载,如下所示是产品信息的DTO对象,除了本身CreateProductDto必须有的字段外,我们另外增加了两个属性,如下代码所示。

然后我们在应用服务接口的ConvertDto转义函数里面增加自己的处理转义逻辑即可,如下代码所示。

        /// <summary>
        ///对记录进行转义/// </summary>
        /// <param name="item">dto数据对象</param>
        /// <returns></returns>
        protected override voidConvertDto(ProductDto item)
{
//如需要转义,则进行重写 #region 参考代码 //用户名称转义 if(item.CreatorUserId.HasValue)
{
//需在ProductDto中增加CreatorUserName属性 item.CreatorUserName=_userRepository.Get(item.CreatorUserId.Value).UserName;
}
if(item.Status.HasValue)
{
item.StatusDisplay
= item.Status.Value == 0 ? "正常" : "停用";
}
#endregion}

这样客户端就可以采用这两个属性展示信息了。

前面也介绍了,对于产品类型属性,我们一般是一个字典信息的,因此我们可以集成绑定字典的处理,如下代码所示。

这个BindDictItems是扩展函数,通过扩展函数,我们对控件类型的绑定字典操作进行处理即可,具体的逻辑代码如下所示。

    /// <summary>
    ///扩展函数封装/// </summary>
    internal static classExtensionMethod
{
/// <summary> ///绑定下拉列表控件为指定的数据字典列表/// </summary> /// <param name="control">下拉列表控件</param> /// <param name="dictTypeName">数据字典类型名称</param> /// <param name="emptyFlag">是否添加空行</param> public static async Task BindDictItems(this ComboBoxEdit control, string dictTypeName, bool isCache = true, bool emptyFlag = true)
{
await BindDictItems(control, dictTypeName, null, isCache, emptyFlag);
}
/// <summary> ///绑定下拉列表控件为指定的数据字典列表/// </summary> /// <param name="control">下拉列表控件</param> /// <param name="dictTypeName">数据字典类型名称</param> /// <param name="defaultValue">控件默认值</param> /// <param name="emptyFlag">是否添加空行</param> public static async Task BindDictItems(this ComboBoxEdit control, string dictTypeName, string defaultValue, bool isCache = true, bool emptyFlag = true)
{
var dict = awaitDictItemUtil.GetDictByDictType(dictTypeName, isCache);

List
<CListItem> itemList = new List<CListItem>();foreach (string key indict.Keys)
{
itemList.Add(
newCListItem(key, dict[key]));
}

control.BindDictItems(itemList, defaultValue, emptyFlag);
}

......

最后我们可以看到,字典列表的效果如下所示。

新增产品信息界面如下所示。

4、基于代码工具的Winform界面快速生成

这些都是标准的Winform界面模板,因此可以利用代码生成工具进行快速开发,利用代码生成工具Database2Sharp快速生成来实现ABP优化框架类文件的生成,以及界面代码的生成,然后进行一定的调整就是本项目的代码了。

ABP框架的基础代码生成我们就不再这里介绍了,主要介绍下Winform展示界面和编辑界面的快速生成即可。

在生成Abp框架的Winform界面面板中,配置我们查询条件、列表展示、编辑展示内容等信息后,就可以生成对应的界面,然后复制到项目中使用即可,整个过程是比较快速的,这些开发便利可是花了我很多反复核对和优化NVelocity模板的开发时间的。

如下是代码生成工具Database2Sharp关于ABP框架的Winform界面配置。

设置好后直接生成,代码工具就可以依照模板来生成所需要的WInform列表界面和编辑界面的内容了,如下是生成的界面代码。

放到VS项目里面,就看到对应的窗体界面效果了。

生成界面后,进行一定的布局调整就可以实际用于生产环境了,省却了很多时间。

在前面两篇随笔《
ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理
》和《
ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程
》开始介绍了权限管理的内容,其中只是列出了内部的权限系统的审计和登陆信息,以及对Winform界面的整合,本篇随笔继续介绍ABP开发框架的权限控制管理内容,包括用户、角色、机构、权限等方面,以及该框架在Winform方面的应用集成。

1、ABP框架的权限控制管理内容

我们知道,权限管理一般都会涉及到用户、组织机构、角色,以及权限功能等方面的内容,ABP框架的基础内容也是涉及到这几方面的内容,其中它们之间的关系基本上是多对多的关系,它们的关系如下所示。

不过在官网下载的框架里面,包含权限管理这些应用服务层和展示层的内容并不完整,只是简单的包括了用户和角色的基础管理,而且很多权限管理所需要的基础功能并没有提供。

根据ABP框架提供的基础数据库表,我们可以进一步整理权限管理几个重要概念和真实数据库表之间的对应关系,基于这个基础上,我们可以完善整个权限管理模块内容。

上图是ABP基础框架中权限模块里面包含的一些主对象表和中间表,中间表主要用来存储两个对象之间的多对多关系,如角色包含多个用户,用户属于多个机构,机构包含多个角色等等。

2、基于ABP框架的权限管理模块

1)组织机构管理

组织机构主要就是一个层级的对象关系,一般包含但不限于公司、部门、工作组等的定义,其中组织机构包含用户成员和角色成员的关系,如下界面所示。

组织机构包含的成员可以添加多个人员记录,添加界面如下所示。

添加角色界面如下所示。

2)角色管理

角色信息没有层级关系,可以通过列表展示。

其中角色包含权限分配和角色成员的维护,如下是角色编辑界面,包含角色基本信息、权限、成员管理等。

角色的权限包含系统可以用的权限,并可以勾选为角色设置所需的功能点,如下界面所示。

用户成员则和机构的用户管理一样,可以指定多个用户。

3)用户管理

用户管理只需要管理用户基本的信息即可,我们如果需要分配角色可以在角色管理里面统一处理。当然,创建用户的时候,也可以ABP框架的收费版本界面一样,为用户指定角色和机构信息。

我这里主要是维护用户信息即可,用户列表界面如下所示。

用户编辑或者查看界面,除了可以看用户基础信息外,可以查看用户包所属的机构(多个),或者所属的角色(多个)

当然可以查看这个用户本身拥有的权限功能点,如下界面所示。

4)权限功能

严格来说,ABP框架并没有统一管理好权限功能点的,它没有任何表来存储这个功能集合,而是通过派生AuthorizationProvider的子类来定义权限功能点,这种需要通过指定AuthorizationProvider的子类的方式创建功能点,需要每次系统模块增加功能点的时候,编码一下,然后增加自己的功能点,如下界面所示。

这种方式可能能够满足大多数的需要,不过我如果需要增量开发,或者动态增加某些功能点的时候,就有点不方便了。

我在这个基础上引入了一个权限功能的表用来存储功能点的,然后提供管理界面来动态维护这些功能点。如下界面所示。

这样我可以动态添加或者批量添加所需要的功能点,并且和整个权限管理模块串联起来,形成一个完整的控制体系。

这些概念主要还是来源于我的Winform开发框架和混合式开发框架里面的控制思路,以及界面展示的处理。

这样我们就可以管理自己的权限功能点,并可以为指定的角色配置相关的控制功能点,如下表所示是角色的权限集合(系统中间表),也就是给角色分配的功能点,依旧是在原来的系统表里面存储。

3、权限控制在业务模块界面中的使用

我们拥有了用户、角色、机构、权限功能以及它们之间的关系后,我们可以按照一个完善的权限系统来创建对应的用户角色权限关系,并通过在客户端对界面权限的判断和服务端对操作权限的判断,实现完整的控制处理。

服务端由ABP框架内置权限进行管理,通过在AppService里面定义好增删改查等权限点,如引用服务层的基类设置了几个权限点的属性。

我们在子类里面指定这些操作的变量即可,如产品应用服务中,我们可以定义CreatePermissionName为 Product/Add 这样的名称,当然也可以自定义。

然后每次在Action中调用相应的检查即可,如下是对创建的判断检查。

或者更新操作的权限检查

如果对于导入、导出等其他权限,我们则可以通过调用

void CheckPermission(string permissionName);

来进行自己自定义权限名称的判断。

在客户端,我们登录成功后,获取用户的权限集合,然后在客户端进行判断即可进行权限的控制管理,可以控制菜单、按钮等界面元素,如下是整合了权限控制的产品信息管理界面。

分页列表展示界面的控制代码如下所示。

编辑或者查看界面的控制代码如下所示

这样我们 就可以整合了权限到业务管理模块里面,实现对菜单、按钮等元素的权限控制了。

首先在权限管理系统模块里面为用户角色添加对应的产品管理权限点。

产品信息界面展示如下所示。

如果在权限模块的角色里面取消对应的功能点,那么产品管理功能不可用。