业务实现是整个系统的核心,控制器是面向用户操作的接口,数据层是面向数据交互的接口。
业务层需要一系列的帮助工具类和插件:
cyb.Utility.Network | 封装api请求的工具类,这个封装是根据框架中的api请求和返回格式而设计的,是分布式事务的核心组件 |
cyb.Utility.Redis | 封装Redis操作的常规方法 |
cyb.Utility.Tools | Json、MemroyCache、TaskQueue、Mapper等常用的工具类 |
cyb.Utility.MQ | RabbitMQ操作类,微服务之间的通讯工具,需要配合MQStarter |
cyb.Utility.Lambda | Lambda表达式操作类 |
cyb.Utility.Job | 定时任务客户端,向定时任务微服务发送一个定时任务回调接口 |
cyb.Utility.IM | 内部消息处理,需要有IMStarter配合才能起效 |
其他工具类也不一一罗列。
业务的结构分两类,关联关系的中间表这种不直接体现在用户操作界面,就不做说明
第一、单个表的业务。例如员工、部门、字典等,一张表就是一个独立的业务逻辑,可能业务之间存在一些逻辑,例如员工属于某一个部门,这样的外键,其实在界面中一个下拉选择就解决了。一般对于存在ID和Name的表,ID是其他表的外键,例如员工表中的部门ID,在员工表中建议冗余部门名称,这也消耗不了多少空间,现在磁盘不值钱,多写点,能大大提高查询性能。在实际业务设计中,也尽量把常常使用到查询中的字段冗余,例如采购明细中的产品计量单位也冗余保存到采购明细中。
第二、主从表的业务,一般都是业务表单,例如销售单、采购单、进仓单、出仓单等,单据操作和单个表的操作不完全一样,例如编辑业务表单,往往都是修改明细的数量和种类,不会修改明细中的其他信息,因此编辑往往转换成了明细的增加、减少这样的操作,顶多就是修改一下数量,而主表的编辑和单个表的操作差不多。业务表单往往会带着业务流程审核,可能需要配合工作流来完成审核过程,业务表单增加两个依赖库
cyb.SDK.Activity | 工作流封装接口 |
cyb.Utility.Stock | 业务表单上下级数量流转运算器 |
业务层是衔接着控制器和数据层,作为两者的桥梁也是业务解析的核心。所以业务层也没有必要完全把数据层的所有方法搬出来,控制器也用不了。
public interface IBaseBLLObject : IBaseObject
{
//RabbitMQ的调用方法,如果不存在MQ插件,则此方法终止执行,不会报错
MQPublish(T data); //fanout发送模式
MQPublish(string topic,T data); //topic模式发送模式
//内部消息发布方法,如果不存在IM的插件,则此方法终止执行,不会报错
IMPublish(string topic,T data);
}
public interface IBaseBLL : IBaseBLLObject
where T:class
{
RValue Insert(T item);
RList Insert(List items);
RValue Update(T item);
RList Update(List items);
RValue Delete(T item);
RList Delete(List items);
//事务,管理当前资源涉及到事务的DAL
void Commit();
void Rollback();
RValue GetEntity(T item); //通过item中的关键字获取整个实体的数据
RList GetList(Dictionary where,string orderBy="");
RPage GetList(Dictionary where,int page,int size,string orderBy="");
}
对于主从表的表单业务层,就不能和单个表的操作一样,毕竟涉及只少两张表,或者存在多个从表的情况,这里不详细说明,经过多年的经验,多个从表业务的不确定性很大。
//这个是主从表的公共实体对象
public class BillEntity
{
public THdr hdr{get;set;}
public List dtls {get;set;}
}
public interface IBaseBillBLL:IBaseBLLObject
where THdr:class,IBaseHdrEntity
where TDtl:class,IBaseDtlEntity
{
RValue> GetBill(string formno);
RValue GetHdr(string formno);
RList GetDtls(string formno);
RList GetHdrList(Dictiongary where,int page,int size,string orderBy="formno desc")
RList GetDtlsList(Dictiongary where,int page,int size,string orderBy="code")
RValue SaveBill(BillEntity bill); //这里包含新建和编辑
RValue Delete(THdr hdr); //删除整张单据
RList Delete(List dtls); //移除明细
RValue CheckBill(THdr hdr); //审核单据,如果涉及工作流,就要重载这个方法,还需要定义一系列的工作流方法
}
配合主从表的定义,主从表都需要定义一些固定的字段,例如单号:Formno,产品编号:Code等,具体字段根据设计和业务要求,自己考虑就可以了。
把表单业务抽象是很困难的事情,只能说,把一些共性的操作稍作抽象而已,其中一点,就是要做一个业务抽象配置
public interface ISetting
{
//自动发布表单变更
bool AutoPublish { get; set; }
//表单的名称或者唯一标识
string BillName { get; set; }
//基础资料中的物品是否可用
//例如当一些产品是准备淘汰,则设置产品Active=false,不能采购这个产品
//但是出库是不受限制
bool CodeActiveLimit { get; set; }
//明细中的编号是否需要唯一,就是是否可以出现两条记录存在相同编号的记录
bool CodeUnique { get; set; }
//单据是否需要审核,或者说参与工作流。有些单据是保存就是起效的
//例如入仓。采购和销售都是需要审核的
bool IsNeedCheckBill { get; set; }
//是否凭证单据,如果是,则在删除的时候则产生冲减单据,否则,直接删除
bool IsProof { get; set; }
}
业务层中很关键一点就是事务,那么,分布式事务怎么办?其实我们换个思路,把API接口也当做数据库操作即可,封装基于API的DAL基类,当然,是针对本框架的,毕竟事务在框架结合非常紧密。那么,类似dbContext,一旦被调用,则自动根据TransactionKey的值来确定是否自动启动事务。每个API资源都会带有Commit()和Rollback()方法,与数据库操作并没有差异,这样,就可以解决掉分布式事务的问题。事务的开始和结束,框架的规则是建议用标注来完成的。例如:
//这里只是示例代码,并不是真实的业务代码
public class BillPur:BaseBillBLL,IBillPur
{
//事务管理,以标注统一管理,不管是微服务被动启用事务,还是主动启用事务,都通过标注内部代码统一管理,程序员完全可以忽略事务的具体细节
[Transaction]
public override Rvalue SaveBill(BillEntity bill)
{
var rHdr = dalhdr.Insert(bill.hdr); //dalhdr中的dbContext会自动创建事务
var rDtl = daldtl.Insert(bill.dtls); //daldtl中的dbContext会引用dalhdr创建的事务
//上述代码,只会产生一个事务对象ITransaction对象
var rApiResult = api_dalPlan.Update(planHdr); //API调用,发送更新请求的时候,会夹带TransactionKey,这样,另外一个微服务也会同步开始事务
return rHdr;
}
//向其他微服务发布进度变更消息
[MQPublish]
[Transaction]
public override RValue CheckBill(PurHdr hdr)
{
//审核代码
var rCheck = base.CheckBill(hdr);
//若是流程最后一步,则调用流程数量运算器,建立上下游数据之间的关系
var rDtls = daldtl.GetList(t=>t.Formno==hdr.Formno);
if(!rDtls)
return rDtls.ToError();
var rRun = CalRunner.Run(rDtls.Value);
if(!rRun)
return rRun.ToError();
return rCheck;
}
//订阅来自基础微服务的员工数据新增消息
[MQConsume("basic","employee","insert")]
public RValue MQInsertEmp(Employee emp)
{
//如果员工是采购员,则保存下来,否则忽略
//忽略代码
}
//订阅来自内部模块的进度消息
[IMConsume("checkprogress")]
public RValue IMSaleProgress(PurHdr hdr)
{
//工作流审核过程,要同步做一些动作,则订阅进度消息就可以了
}
}
这里的事务标注使用了AOP技术,进入方法的时候,检查是否已经存在TransactionKey值,如果存在,则忽略,否则就创建TransactionKey的值,并保存到上下文中。当方法执行完成后,如果RValue的返回值success!=true,或者出现了异常,则调用Rollback方法。这样就很好解决了事务嵌套的问题,程序员也不需要关心事务具体的管理过程。框架的AOP使用的是Fody,使用起来也简单。
在消息这个机制,在生产系统中,感觉特别好用,例如跟踪生产扫描,每个生产工序的扫描动作,都发送一个扫描的消息,当第一个工序扫描,发送start消息,中间工序发送step消息,完成后发送finish消息,这样,在生产计划接收到这些消息,就能准确计算出生产计划的生产进度,同理也能计算出销售单或者产品的生产进度,且这个数据还是很准确的;如果和自动设备对接,消息机制就可以完成数据对接之间的通讯,例如分拣机械手,相当于进行了分拣扫描,做一个插件和机械手对接,并把分拣的结果通过消息机制发回到生产系统。微服务利用消息机制,就可以实时获取不同微服务的流程进度情况,也不用编写这么多代码,还不需要程序员编写协议。关键是MQ消息,可以跨语言应用且并不需要考虑接收端是否在线,只有Queue在,那么接收端再次上线就可以能继续处理消息。
目前就是用这个办法,实现了ERP和IOT之间的通讯,在生产系统中实现了半自动控制。生产系统产生的生产任务,运算后,推送到对应的生产设备上,生产完成的任务,又推送到生产系统,两大系统实现无缝对接。
基于API的DAL封装,也另外封装了基于框架的SDK,包括了登录等一系列的方法,可以用于任何基于dotnet开发的的第三方程序使用,同时也提供了文档说明(包括Postman调用例子)。框架使用插件与外部应用实现对接,也提供了开发包给外部应用对接,具有相当大的开放性。