前言
很多场景【单体+模块化】比微服务更合适,开发难度低、代码可复用性强、可扩展性强。模块化开发有些难点,模块启动与卸载、模块之间的依赖和通讯。asp.net core abp为我们提供了模块化开发能力及其它基础功能。基于abp(一代6.3)结合DDD已基本开发好一个【工单管理模块】,本篇做个基本介绍并说明如何集成此模块。
资源
视频讲解:https://www.bilibili.com/video/BV1ky4y1b79u/
线上demo:http://web1.cqyuzuji.com:9000/ 账号:admin 密码:123qwe
后端源码:https://gitee.com/bxjg1987_admin/abp
前端源码:https://gitee.com/bxjg1987_admin/front
必备知识
熟悉asp.net core和abp(注意是老版本,非vNext,但也很容易迁移到vNext上)
术语
下文会提供到一些概念,理解这个黑重要。
abp模块:这个不解释了,是abp基础,请参考官方文档
通用模块:这个是使用abp模块开发方式做的一些通用的,与具体业务无关的模块,比如:数据字典模块
业务模块:工单管理、广告管理、电商模块等为了实现具体业务的模块。
业务场景
客户是做复印机出租的,它希望做一套系统管理整个业务,其中工单是一个比较重要的模块,大致流程如下:
- 客户通过小程序上报工单,说明什么设备出了什么问题
- 系统后台管理员查看下大致问题后审核
- 后台管理员将已审核的工单分配给指定维修人员,或维修人员通过app自己领取已审核的工单
- 当维修人员到达客户处,通过app将工单设置为已执行状态
- 当维修人员处理完任务后通过app将工单设置为已完成状态,同时可能需要录入完成情况说明
以上是主体流程,还有些边角的以后文章会详细说明,比如:从审核状态跳跃到已完成状态;从已完成状态回退到待审核状态;状态变化时的事件等。
工单类型不同:有些工单可能并不是客户提交的,比如当采购的二手设备入库时要做检修,也会产生工单,这种情况工单不会与客户关联,而时与入库单关联;再比如让某员工开车去托快递回来这种情况工单会与物流信息关联
工单创建方式不同:客户通过小程序提交、后台管理员手动建立、当发生某些事件时自动创建(比如采购入库时自动创建)
其实工单管理模块是个通用的业务,在很多系统可能都需要,因此考虑做成独立的业务模块,方便复用。
目标
可复用
工单模块以nuget包发布,你可以安装后简单配置后就可以使用。
易升级
上面说了,以nuget包形式发布的,将来模块更新后发布新版本的nuget包,各系统更新下,引用新版本包就ok啦
独立性
工单模块只依赖些通用的、非业务型的模块。工单模块需要用到“员工”概念,在系统中往往体现为一个用户,工单模块本身不提供“员工管理”的功能,因为你的系统可能有自己的“员工管理”功能;或你直接拿abp原始的 AbpUser作为员工也行。试想如果“工单模块”本身提供了员工管理模块,你引用过去,发现自己系统中已有实现了的员工管理,是不是很麻烦?
所以你的项目引用工单模块时需要做个适配,为工单模块提供需要用到的员工相关功能,主要是几个查询。
说明:
abp vNext使用契约层来实现模块独立化,个人认为不完整,比如你的项目中有个”员工管理“模块,你在定义契约接口和DTO时只能定义通用的,为了尽量通用,接口中的方法会尽量多,或分开多定义几个接口,DTO中的属性也会尽量多,因为你不知道将来哪各模块引用你,所以你无法定义准确的、刚好够用的接口和DTO。
现在有各”工资管理“模块,引用你的”员工管理模块“,它会拿到DTO中很多不必要的属性,也会在引入接口时拿到很多不需要的方法。
再比如我的”工单模块“如果直接引用你的契约,将来我发布工单模块,其它系统引用后,它必须去实现”员工契约“中的接口,它会很迷茫,我要实现这个契约中所有的接口吗?DTO所有的属性我都需要赋值吗?其实某些契约中的接口方法工单模块可能根本不需要,同理契约中的DTO也不一定都需要赋值。
还有更多问题,这些问题不影响使用,但挺别扭。出现这样的原因,是独立的业务模块应该在契约中定义自己能向外提供什么数据之外,还应该定义自己需要什么,而不是让别的模块的契约来指定。
我们在开发工单模块时,会从这两个方向来定义契约,即:工单模块需要什么数据?工单模块能向外提供什么数据?
可扩展
abp本身提供了很强的扩展能力,你可以
- 通过“动态属性系统”来扩展实体类
- 通过工单CRUD、工单状态变化等事件来添加自己的业务逻辑
- 通过集成并替换工单模块提供领域服务、应用服务来重写现有业务逻辑
- 默认的UI只是结合我自己的项目用easyui实现的,你可以实现自己的UI
- 通过集成抽象工单实体、抽象工单的领域服务、抽象的工单应用服务来实现更多的工单类型
使用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-rc3
XXX.EntityFrameworkCore层中
Install-Package BXJG.WorkOrder.EFCore -Version 1.0.0-rc3
XXX.Application层中
Install-Package BXJG.WorkOrder.Application -Version 1.0.0-rc3
Install-Package BXJG.WorkOrder.EmployeeApplication -Version 1.0.0-rc3
工单模块中,后台管理工单和员工端对工单的操作是分开两个应用层项目定义的,根据你的情况决定是否分开,若分开则上面的包需要分开安装。
配置
在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中注册【普通工单】和【工单分类】的权限,为了演示将权限注册在了租户权限下面。
admin.AddBXJGWorkOrderAllPermission();
同理在BXJG.Web.Mvc/Startup/XXXNavigationProvider中注册【普通工单】和【工单分类】的菜单
context.Manager.MainMenu.AddBXJGWorkOrderAllNav();
默认情况会注册 工单模块的根、工单分类、默认工单的权限和菜单,可以调用另外几个类似名称的扩展方法来分别注册需要的权限和菜单,你也完全可以按abp的方式完全自己定义权限和菜单的注册,这种情况下,若你要使用默认工单的功能,权限的名称必须对应,请参考默认工单权限常量名
注册动态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 {
添加模块适配代码
如前所述,工单模块对员工的依赖是通过IEmployeeAppService接口隔离的,默认将AbpUser作为员工,但设计模块时无法确定你的用户类型,因此需要在你的主程序XXXApplicationModule.Initialize()按如下方式注册服务
IocManager.RegisterBXJGWorkOrderDefaultAdapter<User>();//User为你项目的用户类型
当然这个服务是可以替换的。
另外员工端对工单的管理服务中需要获取当前登陆的员工,因此规定了IEmployeeSession接口,默认使用abp提供的IAbpSession,这个不需要做任何配置,当然你也可以替换这个服务
数据库迁移
这个是按abp的套路,这不再详述。注意abp默认下载来的项目连接字符串是连接到localhost的,而vs2019的localdb稍有不同,我是改成如下形式的,你看着办
"Default": "Server=(localDB)\\mssqllocaldb; Database=BXJGDB; Trusted_Connection=True;"
运行
不出意外的话接口就可以访问了
- WorkOrder:后台管理员对工单进行管理的接口,其中ChangeStatus是将工单跳跃或回退到指定状态,这个操作不是一步到位的,比如从”待审核“状态 跳跃到 ”已完成“中间会经历:确认、分配、执行、完成等步骤,操作员必须有这些步骤的权限,切工单状态必须正确(打个比方,分配时会判断工单是否已关联的处理人,只是假设,目前没做这个限制),这部分逻辑大多在工单实体中。
- WorkOrderCategory:后台管理员对工单分类进行维护的接口
- WorkOrderCommon:员工端或后台管理端都可以调用的接口,用来获取工单状态列表、紧急程度列表等
- WorkOrderEmployee:员工端对工单进行操作的接口,获取待分配的工单、执行、完成工单等。
后续
- 目前已实现真正的独立的业务模块
- 配置还需要进一步简化
- 目前只是基本能跑通工单流程,未作详细测试。
- 上面说明了模块的基本集成,以及模块内默认实现的“普通工单”的功能,如何扩展后续会说明,比如:实现自定义类型的工单、如何通过继承、事件等方式来扩展工单模块等。
- 目前模块只提供到应用层级别,也就是只提供后端接口,前端我使用的easyui,虽然可以使用abp的虚拟文件系统来实现UI模块化,但目前没有这样做,你可以使用自己喜欢的框架来完成UI