用golang开发电商类后台业务

2021-07-01 18:12:00 浏览数 (1)

顺应公司的开发趋势,我们团队选择了golang作为后台开发的主语言,但golang所推崇的简洁,原生,轻量,对于我这种java出身的同学来说,太过原生也就意味着配套的工具链和开发框架的缺失。而恰巧,我们又是一个电商类后台团队,电商后台业务如果用一个词来概括的话,我想应该是复杂。那么如何用简洁的开发模式来应对复杂的业务系统,是我们所面临的一个很大的挑战。

应对复杂系统的解决之道总的来说就是两个:分而治之,抽象模型。

是的,无论多么复杂的事情,这两个就是我们拆解和应对的最大武器,将复杂的系统拆解,并抽象出具体的业务模型,借助这两者,我们才能够很好的抵抗外部的复杂性,并聚焦在最核心的事情上。而这也是为何我们需要ddd,需要设计模式,需要领域模型的原因。

因此,我们团队是在近一年前很多工具缺失的情况下,启动了电商后台的开发。这里分享一下我们是如何应对和解决的

1.分而治之:

无论是系统的微服务划分,还是细粒度到每个函数的定义,分而治之的思想都横贯在开发的始终,具体到工程代码的维度上,我们的第一个方式是分包。

对于一个典型的后台服务来说,它的分包模式我们设计如下图所示:

我们首先明确的是,各个不同子包负责不同的功能,保证了职责的独立和分离;

service:

服务层,作用是对外提供的服务,是数据的入口,包括trpc的接口以及各类消息的消费者等等;

ao:

防腐层,作用是参数转换,逻辑编排,以及部分errocode的封装;

do:

业务领域层,作用是业务的核心领域,负责最核心的业务逻辑实现;

repo:

数据仓库层,负责数据存储查询等功能,包含mysql,redis等各类实现,这里均以接口形式对外提供服务;

integration:

第三方服务层,负责收拢所有的外部接口依赖,包括trpc接口和cmq的生产者等等;

config:

配置层,包括tconf配置,常量,错误码等等

2.抽象模型:

我们将复杂的产品需求抽象成聚焦的业务模型,并把对业务模型的抽象和定义放在do层,用来表示最聚焦的业务模型。

这里我们提供一个提现的case,来说明下各个子包的功能以及具体的实践。

我们业务上会有用户个人钱包的概念,这个钱包要支持收钱、转账、提现等的能力,那我们以提现为例,看下这个代码的具体实现:

面向领域模型开发的一个最重要的地方在于,最早设计开发的不是数据库,而是do层的领域模型;

这里我们设计了三个核心的领域实体:

1.订单;

2.用户账户余额;

3.流水记录;

试想下,无论是怎样的业务场景(收,转账,提现等)都是由这三个“原子”型的模型所组合和定义的。

因此,我们首先在do层设计了这三个实体:

账户:

代码语言:javascript复制
type VirtualAssetBalanceDo interface {
/**
    * @Description    资产状态有效
    * @Author         yang
    * @Date           2020/12/1
    */
   IsValid(ctx context.Context) bool
   /**
    * @Description    获取
    * @Author         yang
    * @Date           2020/6/30
    */
   Get(ctx context.Context) VirtualAssetBalance
   /**
    * @Description    扣除
    * @Author         yang
    * @Date           2020/5/29
    */
   Del(ctx context.Context, Amount uint64) error

   /**
    * @Description    增加
    * @Author         yang
    * @Date           2020/5/29
    */
   Add(ctx context.Context, Amount uint64) error
....
}

订单:

代码语言:javascript复制
type WithDrawOrderCertificateDO interface {
Get(ctx context.Context) WithDrawOrderCertificate
   ChangeCurStatus(ctx context.Context, status string) error
   Create(ctx context.Context) error
   AddVersion(ctx context.Context) error
   FillWXPayInfo(ctx context.Context, item *comm.DetailOrderItem)
}

流水:

代码语言:javascript复制
type RecordInfo struct {
   Id                         uint64
   VirtualAssetID             uint64
   VirtualAccountBusinessType uint64         //账户业务类型 1:小鹅农场金币  10:现金账户
   VirtualAssetType           uint64         //资产类型: 1:现金类账户 2:交易类账户 3:负债类账户
   BeforeBalance              uint64         //更新前余额
   AfterBalance               uint64         //更新后余额
   LastRecordID               uint64         //上次流水的ID
   Amount                     uint64         //本次增加或减少的额度
   RecordType                 RecordInfoEnum // 0:nil 1:收入 2:支出 3.转账
   RecordID                   uint64         //
   FromID                     uint64         //来源ID: income:fromid  outcome:orderid,transfer:transferorderid,
   FromName                   string         //收入来源/支出去向
   RuleID                     uint64         //渠道规则ID
   RuleName                   string         //渠道规则名称
   ExtMsg                     string         //
}

这三个核心领域模型是基石,同时,注意,我们使用的是接口/实现类的模式,我们是面向接口编程的模式,为啥这样,也是为了方便扩展,比如订单,可能会有多种实现类;

在此基础上,我们会设计聚合类:

提现聚合接口:

代码语言:javascript复制
type WithDrawAggregateDO interface {
WithDraw(ctx context.Context) (*WithDrawRspInfo, error)
}

提现聚合接口实现:

代码语言:javascript复制
/**
 * @Description    提现聚合
 * @Author         yang
 * @Date           2020/12/2
 */
type WithDrawAggregateDOImpl struct {
   AssetBalanceDo     do.VirtualAssetBalanceDo
   PayRecord          *do.RecordInfo
   CertificateOrderDO do.WithDrawOrderCertificateDO
}
代码语言:javascript复制
func (s *WithDrawAggregateDOImpl) WithDraw(ctx context.Context) (*do.WithDrawRspInfo, error) {

//1.创建提现订单并预扣,并创建超时任务
   err := s.createOrderAndLockBalance(ctx)
if err != nil {
xlog.WarnContext(ctx, 0, "CreateOrderAndLockBalanceError", "err", err)
return nil, err
   }
//2.调用接口,若出现error则什么都不做,静待超时任务重试
   certificate := s.CertificateOrderDO.Get(ctx)
   rsp, err := integration.WXPayWithDraw(ctx, certificate.WithDrawOrderID, certificate.UserID, certificate.Amount)
if err != nil {
xlog.WarnContext(ctx, 0, "WXPayWithDrawError", "err", err)
return nil, err
   }
//rsp为空即可认为rsp为fail
   if rsp == nil {
xlog.WarnContext(ctx, 0, "WXPayWithDrawError", "err", err)
return nil, errors.New("WXPayWithDrawEmptyRSP")
   }
//3.rebuild VirtualAssetBalanceDo 和WithDrawOrderCertificateDO,主要是前面持久化了之后需要version 1,省去查询
   s.AssetBalanceDo.AddVersion(ctx)
s.CertificateOrderDO.AddVersion(ctx)

//4.根据接口返回决定实扣、回滚或什么都不做
   err = DealWithByStatus(ctx, s, rsp.Item)
if err != nil {
xlog.WarnContext(ctx, 0, "更新失败", "e", err)
return nil, err
   }
return &do.WithDrawRspInfo{
      WithDrawOrderID: s.CertificateOrderDO.Get(ctx).WithDrawOrderID,
      Status:          s.CertificateOrderDO.Get(ctx).CurState,
      WxErrorMsg:      s.CertificateOrderDO.Get(ctx).PayErrDesc,
      WxErrorCode:     s.CertificateOrderDO.Get(ctx).PayErrCode,
   }, nil
}

这里可以看到,很明显的,接口实现类中有刚刚上面提到的三个实体,订单、余额和流水,同时在这个聚合实现类里面做具体的实现;

OK,那么问题来了,这个接口实现类是怎么生成的呢?这三个实体哪里来的呢?

这个时候,就是工厂模式的思想出现了,我们作为do层的业务领域,是不需要关注所依赖的实体是从哪里来的,只要关心在拥有了实体之后具体的业务逻辑怎么处理。

这个道理有点像是在厨房做菜,大厨就是我们的do层的聚合类,他只要加工食材就好,至于食材的采购,择洗,他不需要关心,这就是所谓的构建和执行分离,也是工厂模式的思想核心;

那么回到问题里来,实体哪里来呢?

我们将实体的构建和生成,放在factory里面,是的,golang并没有类似spring的bean管理框架(或者有了我也不知道

0 人点赞