abp加DDD开发:低耦合、可复用、可扩展的【工单】业务模块-简介和集成

  • A+
所属分类:.NET技术
摘要

很多场景【单体+模块化】比微服务更合适,开发难度低、代码可复用性强、可扩展性强。模块化开发有些难点,模块启动与卸载、模块之间的依赖和通讯。asp.net core abp为我们提供了模块化开发能力及其它基础功能。基于abp(一代6.3)结合DDD已基本开发好一个【工单管理模块】,本篇做个基本介绍并说明如何集成此模块,后续会详细说明思路。


abp加DDD开发:低耦合、可复用、可扩展的【工单】业务模块-简介和集成

前言

很多场景【单体+模块化】比微服务更合适,开发难度低、代码可复用性强、可扩展性强。模块化开发有些难点,模块启动与卸载、模块之间的依赖和通讯。asp.net core abp为我们提供了模块化开发能力及其它基础功能。基于abp(一代6.3)结合DDD已基本开发好一个【工单管理模块】,本篇做个基本介绍并说明如何集成此模块,后续会详细说明思路。

资源

线上demo:http://web1.cqyuzuji.com:9000/  账号:admin 密码:123qwe

后端源码:https://gitee.com/bxjg1987/abp

前端源码:https://gitee.com/bxjg1987/front

必备知识

熟悉asp.net core和abp(注意是老版本,非vNext,但也很容易迁移到vNext上)

术语

下文会提供到一些概念,理解这个黑重要。

abp模块:这个不解释了,是abp基础,请参考官方文档

通用模块:这个是使用abp模块开发方式做的一些通用的,与具体业务无关的模块,比如:数据字典模块

业务模块:工单管理、广告管理、电商模块等为了实现具体业务的模块。

业务场景

客户是做复印机出租的,它希望做一套系统管理整个业务,其中工单是一个比较重要的模块,大致流程如下:

  1. 客户通过小程序上报工单,说明什么设备出了什么问题
  2. 系统后台管理员查看下大致问题后审核    
  3. 后台管理员将已审核的工单分配给指定维修人员,或维修人员通过app自己领取已审核的工单
  4. 当维修人员到达客户处,通过app将工单设置为已执行状态
  5. 当维修人员处理完任务后通过app将工单设置为已完成状态,同时可能需要录入完成情况说明

以上是主体流程,还有些边角的以后文章会详细说明,比如:从审核状态跳跃到已完成状态;从已完成状态回退到待审核状态;状态变化时的事件等。

工单类型不同:有些工单可能并不是客户提交的,比如当采购的二手设备入库时要做检修,也会产生工单,这种情况工单不会与客户关联,而时与入库单关联;再比如让某员工开车去托快递回来这种情况工单会与物流信息关联

工单创建方式不同:客户通过小程序提交、后台管理员手动建立、当发生某些事件时自动创建(比如采购入库时自动创建)

其实工单管理模块是个通用的业务,在很多系统可能都需要,因此考虑做成独立的业务模块,方便复用。

目标

可复用

工单模块以nuget包发布,你可以安装后简单配置后就可以使用。

易升级

上面说了,以nuget包形式发布的,将来模块更新后发布新版本的nuget包,各系统更新下,引用新版本包就ok啦

独立性

工单模块只依赖些通用的、非业务型的模块。工单模块需要用到“员工”概念,在系统中往往体现为一个用户,工单模块本身不提供“员工管理”的功能,因为你的系统可能有自己的“员工管理”功能;或你直接拿abp原始的 AbpUser作为员工也行。试想如果“工单模块”本身提供了员工管理模块,你引用过去,发现自己系统中已有实现了的员工管理,是不是很麻烦?

所以你的项目引用工单模块时需要做个适配,为工单模块提供需要用到的员工相关功能,主要是几个查询。

说明:

abp vNext使用契约层来实现模块独立化,个人认为不完整,比如你的项目中有个”员工管理“模块,你在定义契约接口和DTO时只能定义通用的,为了尽量通用,接口中的方法会尽量多,或分开多定义几个接口,DTO中的属性也会尽量多,因为你不知道将来哪各模块引用你,所以你无法定义准确的、刚好够用的接口和DTO。

现在有各”工资管理“模块,引用你的”员工管理模块“,它会拿到DTO中很多不必要的属性,也会在引入接口时拿到很多不需要的方法。

再比如我的”工单模块“如果直接引用你的契约,将来我发布工单模块,其它系统引用后,它必须去实现”员工契约“中的接口,它会很迷茫,我要实现这个契约中所有的接口吗?DTO所有的属性我都需要赋值吗?其实某些契约中的接口方法工单模块可能根本不需要,同理契约中的DTO也不一定都需要赋值。

还有更多问题,这些问题不影响使用,但挺别扭。出现这样的原因,是独立的业务模块应该在契约中定义自己能向外提供什么数据之外,还应该定义自己需要什么,而不是让别的模块的契约来指定

我们在开发工单模块时,会从这两个方向来定义契约,即:工单模块需要什么数据?工单模块能向外提供什么数据?

可扩展

abp本身提供了很强的扩展能力,你可以

  1. 通过“动态属性系统”来扩展实体类
  2. 通过工单CRUD、工单状态变化等事件来添加自己的业务逻辑
  3. 通过集成并替换工单模块提供领域服务、应用服务来重写现有业务逻辑
  4. 默认的UI只是结合我自己的项目用easyui实现的,你可以实现自己的UI
  5. 通过集成抽象工单实体、抽象工单的领域服务、抽象的工单应用服务来实现更多的工单类型

使用DDD开发方式

实践下DDD

核心业务逻辑在工单实体类中,它定义了相应的业务方法,内部会改变工单实体自身的一些状态,必要时触发相应事件,以此来确保工单始终能处于正确的状态,比如:某个已完成的工单无关联的员工或没有开始和完成时间;再比如某个已拒绝的工单,没有拒绝说明。如果实体的属性都是public get; set; 很容易出现这种问题,因为协作开发时别人很可能胡乱调用你的实体,随意设置值。

领域服务有少量代码,也触发相应的领域事件。

应用服务来接收前端调用,协调领域实体和服务来实现业务逻辑。

关于DDD下篇详细说明设计思路时再细说

集成

可扩展性中提到工单是抽象化的,但默认提供了一个”普通工单“的实现,因此安装并配置模块后此功能立即可用。另外也可用提供几个子类实现一个自定义类型的工单。

线上demo:http://web1.cqyuzuji.com:9000/  账号:admin 密码:123qwe

先在abp官方下载一个干净的abp项目,写此文章时用的abp6.3 .net 5。或者你可用在你目前的项目引入并测试。按以下步骤进行配置。

安装nuget包

相关nuget包都是以:BXJG.WorkOrder为前缀的。

先确保:

在解决方案上右键 > 管理解决方案的包 > 更新 -> Castle.Windsor.MsDependencyInjection 升级到3.4.0

在解决方案上右键 > 管理解决方案的包 > 更新 -> Microsoft.EntityFrameworkCore 更新到5.0.4

XXX.Core层中

Install-Package BXJG.WorkOrder.EFCore -Version 1.0.0-rc

XXX.EntityFrameworkCore层中

Install-Package BXJG.WorkOrder.EFCore -Version 1.0.0-rc

XXX.Application层中

Install-Package BXJG.WorkOrder.Application -Version 1.0.0-rc
Install-Package BXJG.WorkOrder.EmployeeApplication -Version 1.0.0-rc

工单模块中,后台管理工单和员工端对工单的操作是分开两个应用层项目定义的,根据你的情况决定是否分开,若分开则上面的包需要分开安装。

配置

在DbContext中注册相关实体

由于工单模块没有使用独立DbContext的方式,因此需要在你的主程序的DbContext中注册并配置”普通工单“和“工单分类”的实体。在XXX.EntityFrameworkCore层中找到你的DbContext,做如下配置:

1         public virtual DbSet<BXJG.WorkOrder.WorkOrderCategory.CategoryEntity> BXJGWorkOrderCategory { get; set; } 2         public virtual DbSet<BXJG.WorkOrder.WorkOrder.OrderEntity> BXJGWorkOrder { get; set; } 3  4         protected override void OnModelCreating(ModelBuilder modelBuilder) 5         { 6             base.OnModelCreating(modelBuilder); 7             modelBuilder.ApplyConfigurationBXJGWorkOrder();//别忘了这里的映射配置 8         }

注册权限和菜单

普通工单后台管理和员工端相关权限已定义为扩展方法,可以直接在主程序中调用,将其注册到主程序的权限树中。在XXX.Core/Authorization/XXXAuthorizationProvider中注册【普通工单】和【工单分类】的权限,为了演示将权限注册在了租户权限下面。

1 //注意这里的admin是指你已经存在的权限节点 2 admin.AddBXJGWorkOrderPermission(); 3 admin.AddBXJGEmployeeWorkOrderPermission();

同理在BXJG.Web.Mvc/Startup/XXXNavigationProvider中注册【普通工单】和【工单分类】的菜单

context.Manager.MainMenu.AddBXJGWorkOrderNavigation();

注册动态api

由于开发模块时不确定你会如何使用工单模块的应用层,因此默认并未自动注册为动态web api,如果需要你可以自己配置,目前是手动,将来可能提供扩展方法一次性注册。在XXX.Web.Core/XXXWebCoreModule的PreInitialize()中配置启用工单模块中普通工单和工单分类的相关动态web api

1 //注册后台管理工单的动态api            
Configuration.Modules.AbpAspNetCore().CreateControllersForAppServices(typeof(BXJG.WorkOrder.ApplicationModule).Assembly,"bxjgworkorder");
2 //注册后台和员工端管理工单的动态api
Configuration.Modules.AbpAspNetCore().CreateControllersForAppServices(typeof(BXJG.WorkOrder.BXJGCommonApplicationModule).Assembly, "bxjgworkorder");
3 //注册员工端管理工单的动态api
Configuration.Modules.AbpAspNetCore().CreateControllersForAppServices(typeof(BXJG.WorkOrder.BXJGWorkOrderEmployeeApplicationModule).Assembly, "bxjgworkorder");

添加模块依赖

虽然已添加了模块相关包引用,但此时这些包对于主程序来说仅仅是普通的dll,必须按abp的方式,让主程序的模块依赖工单模块,这样工单模块中的dll才会以Abp模块方式启动。由于工单模块只提供到应用程序级别,因此在主程序的Application层中的Module类中添加依赖是最合适的。在XXX.Application/XXXApplicationModule中添加模块依赖

    [DependsOn(略...                typeof(BXJG.WorkOrder.ApplicationModule),              
typeof(BXJG.WorkOrder.BXJGWorkOrderEmployeeApplicationModule))] public class XXXApplicationModule : AbpModule {

添加模块适配代码

如前所述,工单模块不提供员工管理功能,但依赖员工信息,因此你需要提供一个适配,这也是完全独立的工单模块的关键。在XXXApplication中新增WorkOrder文件夹,然后定义如代码

 1     public class EmployeeAppService : IEmployeeAppService  2     {  3         private readonly IRepository<User, long> userRepository;  4         public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; } = NullAsyncQueryableExecuter.Instance;  5         public EmployeeAppService(IRepository<User, long> userRepository)  6         {  7             this.userRepository = userRepository;  8         }  9  10         public async Task<IEnumerable<EmployeeDto>> GetByIdsAsync(params string[] ids) 11         { 12             var query = userRepository.GetAll() 13                                       .Where(c => ids.Contains(c.Id.ToString())) 14                                       .Select(c => new EmployeeDto 15                                       { 16                                           Id = c.Id.ToString(), 17                                           Name = c.Name, 18                                           Phone = c.PhoneNumber 19                                       }); 20             return await AsyncQueryableExecuter.ToListAsync(query); 21         } 22  23         public async Task<IEnumerable<string>> GetIdsByKeywordAsync(string keyword) 24         { 25             var query = userRepository.GetAll() 26                                       .WhereIf(!keyword.IsNullOrEmpty(), c => c.Name.Contains(keyword) || c.PhoneNumber.Contains(keyword)) 27                                       .Select(c => c.Id.ToString()); 28             return await AsyncQueryableExecuter.ToListAsync(query); 29         } 30  31         public async Task<IEnumerable<EmployeeDto>> GetAllAsync(string keyword) 32         { 33             var query = userRepository.GetAll() 34                                       .WhereIf(!keyword.IsNullOrEmpty(), c => c.Name.Contains(keyword) || c.PhoneNumber.Contains(keyword)) 35                                       .Select(c => new EmployeeDto 36                                       { 37                                           Id = c.Id.ToString(), 38                                           Name = c.Name, 39                                           Phone = c.PhoneNumber 40                                       }); 41             return await AsyncQueryableExecuter.ToListAsync(query); 42         } 43     } 44     public class EmployeeSession : IEmployeeSession 45     { 46         private readonly IAbpSession abpSession; 47  48         public EmployeeSession(IAbpSession abpSession) 49         { 50             this.abpSession = abpSession; 51         } 52  53         public string CurrentEmployeeId => abpSession.UserId?.ToString(); 54     }

当然还需要在XXXApplication中的Model文件的Initialize()中配置依赖注入

IocManager.Register<IEmployeeAppService, WorkOrder.EmployeeAppService>(DependencyLifeStyle.Transient); IocManager.Register<IEmployeeSession, EmployeeSession>(DependencyLifeStyle.Transient);

数据库迁移

这个是按abp的套路,这不再详述。注意abp默认下载来的项目连接字符串是连接到localhost的,而vs2019的localdb稍有不同,我是改成如下形式的,你看着办

"Default": "Server=(localDB)\mssqllocaldb; Database=BXJGDB; Trusted_Connection=True;"

运行

不出意外的话接口就可以访问了

abp加DDD开发:低耦合、可复用、可扩展的【工单】业务模块-简介和集成  abp加DDD开发:低耦合、可复用、可扩展的【工单】业务模块-简介和集成

  • WorkOrder:后台管理员对工单进行管理的接口,其中ChangeStatus是将工单跳跃或回退到指定状态,这个操作不是一步到位的,比如从”待审核“状态 跳跃到 ”已完成“中间会经历:确认、分配、执行、完成等步骤,操作员必须有这些步骤的权限,切工单状态必须正确(打个比方,分配时会判断工单是否已关联的处理人,只是假设,目前没做这个限制),这部分逻辑大多在工单实体中。
  • WorkOrderCategory:后台管理员对工单分类进行维护的接口
  • WorkOrderCommon:员工端或后台管理端都可以调用的接口,用来获取工单状态列表、紧急程度列表等
  • WorkOrderEmployee:员工端对工单进行操作的接口,获取待分配的工单、执行、完成工单等。 

后续

  • 目前已实现真正的独立的业务模块
  • 配置还需要进一步简化
  • 目前只是基本能跑通工单流程,未作详细测试。
  • 上面说明了模块的基本集成,以及模块内默认实现的“普通工单”的功能,如何扩展后续会说明,比如:实现自定义类型的工单、如何通过继承、事件等方式来扩展工单模块等。
  • 目前模块只提供到应用层级别,也就是只提供后端接口,前端我使用的easyui,虽然可以使用abp的虚拟文件系统来实现UI模块化,但目前没有这样做,你可以使用自己喜欢的框架来完成UI