C# 基于微服务开发框架的设计思路(七)-业务层的实现

业务实现是整个系统的核心,控制器是面向用户操作的接口,数据层是面向数据交互的接口。

业务层需要一系列的帮助工具类和插件:   

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调用例子)。框架使用插件与外部应用实现对接,也提供了开发包给外部应用对接,具有相当大的开放性。

你可能感兴趣的:(微服务,架构,云原生)