您的位置:首页 > 其它

三、基础功能模块,用户类别管理——锁、EF并发处理、领域服务、应用服务的划分

2016-11-19 23:51 706 查看
在上一章节中,我们处理了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)主要代码简析

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

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中,发送到前台。再在前台进行数据类型转换并输出。如此做法主要有两个个弊端:

Controller过于重型化,不利于代码质量控制。在MVC中,Controller应只负责基础效验和Action的跳转。

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());
}
}
}


[b]章节过长,未完待续。另征求意见:如此叙述,是否过于繁琐?如大家普遍认为啰嗦,后续我将省略大部分代码解释及配图说明。只保留关键代码说明及设计思路说明。[/b]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐