zoukankan      html  css  js  c++  java
  • 三、基础功能模块,用户类别管理——锁、EF并发处理、领域服务、应用服务的划分

    在上一章节中,我们处理了MVC多级目录问题,参见《二、处理MVC多级目录问题——以ABP为基础架构的一个中等规模的OA开发日志》。从这章开始,我们将进入正式的开发过程。首先,我们要完成系统的基础设置模块(在后续的功能中,需要大量使用这些基础设置信息)。和一般的OA系统不同,在律所OA系统中,用户类别管理是基础模块中非常重要、使用频率非常高的一个基础模块。虽然此功能只是很小的一个字典项设置,但是其中涉及了锁、并发处理、领域服务于应用服务的划分等繁琐问题。

    UI功能页面介绍(因用户功能未完成,欠缺删除页面)

    UI方面,我们使用了Metronic+EasyUI做为主要呈现方式。其中我们对EasyUI做了相应调整,已使其更加适用于Metronic风格。其中,上图所示的cnblogs.scss为本博客的UI风格(仿小米风格,未完工);color.scss为全局颜色设置;common.scss为全局公用css;easyui.custom.scss为我们对EasyUI的样式修改;metronic.scss为我们对Metronic的样式调整; site.scss为程序主css文件。我们在common.scss文件中,导入了color.scss。在site.scss文件中,导入了common.scss、metronic.scss和easyui.custom.scss文件。这样在布局页引入css文件时,我们仅需要引入单个site.min.css文件即可,而不用引入一大堆css文件。我们在布局页中,大量使用了存储于cdn上的一些css和js文件。这些大多都是一些公用类库,大家可根据需要自行下载。

    ABP对CSRF/XSRF跨站攻击的处理

    abp通过token验证以解决上述攻击问题。只要在模板文件(布局页)中,增加@{SetAntiForgeryCookie();},即可方便在ABP内置的ajax辅助方法中发送生成的token。但在实际使用中,我们需要做一些处理。ABP的view页面文件继承自View目录下EasyFastWebViewPageBase类(该类最终继承自AbpWebViewPage类),而SetAntiForgeryCookie()方法则是AbpWebViewPage类的内置方法。所以,要使用SetAntiForgeryCookie()方法。我们必须要所有的布局页全部继承自EasyFastWebViewPageBase类。

    如图所示,在View根目录下,有EasyFastWebViewPageBase.cs文件(依据您的项目名,此处会有不同的文件名)。为了保证在Areas中的布局页也能正常的使用SetAntiForgeryCookie()方法。需要在布局页继承该类。(也可以复制一遍,放到所有Areas的View目录下,但显然,继承的方式更合理)感谢ABP架构交流群的朋友们,刚开始时作者也被这里卡了很久,是群里的朋友们最终指出了问题所在。

    实体设置

    我们在Core层下创建Entities目录(目录名可以随意起,这里采用的常用习惯Entity的复数形式)。然后创建BaseEntity和UserType两个类。

    namespace EasyFast.Core.Entities
    {
        public class BaseEntity : FullAuditedEntity<long>
        {
            public BaseEntity()
            {
                OrderId = 999;
                Guid = Guid.NewGuid();
            }
    
            /// <summary>
            /// model的Guid,用于记录操作日志
            /// </summary>
            public Guid Guid { get; set; }
    
            /// <summary>
            /// 排序Id
            /// </summary>
            [Range(1,999)]
            public int OrderId { get; set; }
    
            /// <summary>
            /// 行号,用于乐观并发控制
            /// </summary>
            [Timestamp]
            public byte[] RowVersion { get; set; }
        }
    }
    namespace EasyFast.Core.Entities
    {
        public class UserType : BaseEntity
        {
            /// <summary>
            /// 人员类别名称
            /// </summary>
            [StringLength(50)]
            public string Name { get; set; }
    
            /// <summary>
            /// 备注信息
            /// </summary>
            public string Remarks { get; set; }
            public ICollection<User> User { get; set; }
        }
    }

    BaseEntity类将做为我们大多数实体的基类。该基本继承自FullAuditedEntity<long>。这样一来,BaseEntity就自动继承了 public long Id{ get; set; }这个属性。我们追加的Guid字段用于记录操作日志。这个在日后使用时再详细说明。RowVersion字段用于对EF进行并发控制。在SQLServer中,行中的数据每变动一次,RowVersion自动+1(该字段为16进制)。通过对比该字段的变化,我们即可得知在修改或是删除数据时,是否存在并发冲突。

    应用层(EasyFast.Application)主要代码简析

    特别提醒:本人对应用层、领域层的讲解仅仅只是本人的一点浅见,不代表DDD的最佳实践要求这么干。在本系列文章里,我们更关注解决工程问题,而不是进行理论研究。如您发现我们的设计有不合理之处,或是对ABP的使用或理解有不对之处。欢迎批评指正。

    Application层一般称之为应用服务于层。在DDD设计规范里,此层专门针对页面进行服务。这个说法可能让人费解。我们举个实际的例子做参考:在OA系统中,我们要展示一个律师的信息时,既要展示User表本身的信息,也要同时展示其关联的Case表、Client表、Finance表等内容。在N层架构的做法中,我们会分别实例化User、Case、Client等对应的业务逻辑类。然后将其查询的结果存储成ViewBag发送到View页面。接着再在View页面中,将对应的ViewBag转换成model进行输出。

            public ActionResult Index()
            {
                long UserID = User.GetUserInfo().LawyerId;
                var user = LawyerService.Find(UserID);
                if (user.Status == JingShOnline.Models.Enum.LawyerStatus.Normal)
                {
                    return RedirectToAction("Normal", "Authen");
                }
    
                ViewBag.CaseList = CaseService.Where(o =>o.LawyerId== UserID)
                    .Include(o=>o.CaseReason)
                    .Include(o=>o.Practice)
                    .Include(o=>o.Court)
                    .Include(o=>o.Industry)
                    .Take(10).ToList();
    
                var history = user.LawyerWorkHistory.OrderByDescending(o => o.StartDate).ToList();
                var education = user.LawyerEducation.OrderByDescending(o => o.StartDate).ToList();
                var academic = user.LawyerAcademic.OrderByDescending(o => o.Id).ToList();
                var certificate = user.LawyerCertificate.OrderByDescending(o => o.Id).ToList();
                var socialposition = user.LawyerSocialPosition.OrderByDescending(o => o.Id).ToList();
    
                //简历完整度
                var ResumeCompletion = (history.Count > 0 ? 35 : 0) + (education.Count > 0 ? 35 : 0) + (academic.Count > 0 ? 10 : 0) + (certificate.Count > 0 ? 10 : 0) + (socialposition.Count > 0 ? 10 : 0);
    
                ViewBag.WorkHistory = history;
                ViewBag.Education = education;
                ViewBag.Academic = academic;
                ViewBag.Certificate = certificate;
                ViewBag.SocialPosition = socialposition;
                ViewBag.ResumeCompletion = ResumeCompletion;
    
                return View(user);
            }
    

      参见上述代码。View页面需要多个模块的数据做集中展示。为达到目的,只好在Controller里初始化多个Service,进行多次查询,然后将查询的结果存储到ViewBag中,发送到前台。再在前台进行数据类型转换并输出。如此做法主要有两个个弊端:

    1. Controller过于重型化,不利于代码质量控制。在MVC中,Controller应只负责基础效验和Action的跳转。
    2. ViewBag是弱类型的,前台使用时,容易出错。且日后代码进行扩展或是重构时,将大大增大bug出现几率。

    Application层的出现,其实就是为了解决这两个问题。在DDD设计规范中,Application针对View进行服务,View需要什么类型的数据,那么Application就返回什么类型的数据。在本例子中,Application会返回一个包含了上述所有ViewBag类型的综合model。这样就可以把大量代码转移到Application或是Core层去实现,且前台只接受一个含有具体数据的model,model是强类型的,不用考虑数据类型转换问题。

    在ABP里,作者推荐为每一个应用服务单独建立目录,且每一个应用服务目录中都应包含Dto子目录。该目录用于存放ViewModel。Application层负责将从Core或是Repository中得到的Entity转化成ViewModel,发送到前台。参见上图,我们将UserType这个应用服务单独创建目录(单独将UserType视为一个应用服务其实不太合理,更合理的做法是将和User相关的所有内容统一在User这个应用服务中实现)。Dto目录里存放着UserTypeAppService中每一个方法所对应的输入输出参数。ABP推荐将Application中的服务方法所需的参数全部类型化。并分别以Input、Output结尾以做区分。比如我们的删除用户类别方法,需要两个参数:long oldId, long newId,我们仍旧把这两个参数组成一个类去传递。这样做的好处是,日后进行重构或是功能调整时,会大幅减少程序中的修改地方。降低出现bug的几率。

    namespace EasyFast.Application.UserType
    {
        public interface IUserTypeAppService : IApplicationService
        {
            UserTypeInput Find(long id);
            long Add(UserTypeInput model);
            long Update(UserTypeInput model);
            EasyUIDataGrid<UserTypeDataGridDto> GetDataGrid(UserTypeSearch search);
            /// <summary>
            /// 检测传入的全部用户类别是否含有用户,用于判断直接删除or转移用户后再删除
            /// </summary>
            /// <param name="ids">long[] Model.UserType.Id</param>
            /// <returns>true:含有用户 false:不含用户</returns>
            bool CheckIsHaveUser(long[] ids);
            void Delete(DeleteInput model);
        }
    }
    
    namespace EasyFast.Application.UserType
    {
        public class UserTypeAppService : EasyFastAppServiceBase, IUserTypeAppService
        {
            private readonly IRepository<Core.Entities.UserType, long> _userTypeRepository;
            private readonly IUserTypeService _userTypeService;
    
    
            public UserTypeAppService(IRepository<Core.Entities.UserType, long> userTypeRepository, IUserTypeService userTypeService)
            {
                _userTypeRepository = userTypeRepository;
                _userTypeService = userTypeService;
            }
    
            public UserTypeInput Find(long id)
            {
                var data = _userTypeRepository.FirstOrDefault(id);
                return Mapper.Map<UserTypeInput>(data);
            }
    
            public long Add(UserTypeInput model)
            {
                var data = Mapper.Map<Core.Entities.UserType>(model);
                return _userTypeService.Add(data);
            }
    
            public long Update(UserTypeInput model)
            {
                var data = Mapper.Map<Core.Entities.UserType>(model);
                return _userTypeService.Update(data);
            }
    
            public EasyUIDataGrid<UserTypeDataGridDto> GetDataGrid(UserTypeSearch search)
            {
                var data = _userTypeRepository.GetAll()
                    .Where(o => o.Name.Contains(search.Name), !string.IsNullOrEmpty(search.Name));
                var total = data.Count();
                var list = Mapper.Map<List<UserTypeDataGridDto>>(data);
                var rows = list.OrderBy(String.Format("{0} {1}", search.Sort, search.Order))
                    .Skip((search.Page - 1) * search.Rows).Take(search.Rows).ToList();
                return new EasyUIDataGrid<UserTypeDataGridDto> { total = total, rows = rows };
            }
    
            public bool CheckIsHaveUser(long[] ids)
            {
                return _userTypeRepository.GetAllIncluding(o => o.User).Where(o => ids.Contains(o.Id)).Any(o => o.User != null);
            }
    
            public void Delete(DeleteInput model)
            {
                _userTypeService.Delete(model.OldId, model.NewId);
            }
        }
    }

    ABP要求给所有应用服务提取接口,并且接口要继承自IApplicationService。只有继承了这个接口,ABP才会自动实现依赖注入。在UserTypeAppService类中,我们自动注入了UserTypeService这个领域服务和UserTypeRepository这个仓储。除了使用构造参数的注入方式外,您也可以使用属性注入,但构造参数注入显得更高大上一点。在作者理解,简单功能,应用服务直接调用仓储接口实现。复杂功能(尤指业务逻辑代码)在领域服务中实现(Core中的Service),然后应用服务调用领域服务的处理结果,返回给用户。其中,部分功能通过系统默认的仓储接口无法实现的,就自定义仓储然后根据情况,选择应用服务或是领域服务调用并返回。

    在我们的设计实现中,新增或是修改人员类别时,要保证不重名,我们没有采用数据库唯一性约束,而是通过代码实现的,先重名检测,再进行增、改这部分代码属于业务逻辑。所以我们将这些代码放在了领域服务层去实现,应用服务本身不处理这些,其通过调用领域服务中对应的方法并返回合适的结果,供前台使用。

    因ViewModel(或者称为Dto,一个意思,两种常见名称)只在Web和Application中使用,所以将AutoMapper相关的映射代码防止在Application层中最合适不过。我们可以新建一个AutoMapperConfig类,并在其中配置好映射关系后,直接在EasyFastApplicationModule.cs文件中调用即可。不用再web项目中的Global.asax中再次调用,ABP会自动在应用程序初始化时加载我们的配置文件。

    namespace EasyFast.Application.AutoMapper
    {
        public static class AutoMapperConfig
        {
            public static void Bind(IMapperConfigurationExpression opt)
            {
                #region UserType
                opt.CreateMap<Core.Entities.UserType, UserTypeInput>();
                opt.CreateMap<UserTypeInput, Core.Entities.UserType>()
                    .ForMember(d => d.User, s => s.Ignore());
                opt.CreateMap<Core.Entities.UserType, UserTypeDataGridDto>()
                    .ForMember(d => d.UserCount, s => s.MapFrom(o => o.User.Count));
                #endregion
    
                //Mapper.AssertConfigurationIsValid();//验证所有的映射配置是否都正常
            }
    
            public static void Config()
            {
                Mapper.Initialize(Bind);
            }
        }
    }
    
    namespace EasyFast.Application
    {
        [DependsOn(typeof(EasyFastCoreModule), typeof(AbpAutoMapperModule))]
        public class EasyFastApplicationModule : AbpModule
        {
            public override void PreInitialize()
            {
                Configuration.Modules.AbpAutoMapper().Configurators.Add(mapper =>
                {
                    AutoMapperConfig.Bind(mapper);
                    //Add your custom AutoMapper mappings here...
                    //mapper.CreateMap<,>()
                });
            }
    
            public override void Initialize()
            {
                IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
            }
        }
    }
    

    章节过长,未完待续。另征求意见:如此叙述,是否过于繁琐?如大家普遍认为啰嗦,后续我将省略大部分代码解释及配图说明。只保留关键代码说明及设计思路说明。

  • 相关阅读:
    二分 || UOJ 148 跳石头
    等边n边型
    激光样式
    n个数中选k个数和为sum
    引爆炸弹
    光盘行动
    (二分)分蛋糕问题
    总结
    个人测试
    第三次团队作业
  • 原文地址:https://www.cnblogs.com/brucelee/p/6081890.html
Copyright © 2011-2022 走看看