智能风控决策引擎系统代码实现篇(九)Dsl、INode接口、DecisionFlow、Pipeline上下文定义

前言:概念解析

  • DSL: 读取yaml文件后对应的结构体,还需要转换为DecisionFlow才是真正可以执行的图

在这里插入图片描述

  • DecisionFlow: 可实际执行决策流(图),FlowNode记录了具体的一个节点
    在这里插入图片描述

类比到DB存储,则是DB一条记录是一个DSL元信息配置,我们将其加载到内存后用结构体(DSL)保存,而后这个结构体需要再转换为另一个结构体(DecisionFlow)方可执行。可以将Dsl理解为Strategy元信息,对应DB存储,但又不完全一致,如字段类型不一致,如Strategy的某个struct属性字段,json序列化后按string存储到DB。所以又会分别定义common.Strategy(Vo)和resource.Strategy(Po),并提供相关ConvertXXToYY函数让他们可以互相转换,如果需要传输,如HTTP或RPC,则可能还会提供AdaptXXToYY函数转换为Dto,,而DecisionFlow相当于可执行的Graph。

以下是在Go语言中对BO(Business Object,业务对象)、VO(Value Object,值对象)、PO(Persistent Object,持久化对象)、DTO(Data Transfer Object,数据传输对象)概念的解释:

一、BO(业务对象)

1. 概念
Go中,BO是对业务逻辑进行抽象和封装的对象。它包含了与业务规则相关的属性和方法。BO主要关注业务逻辑的实现,可能会涉及多个实体或者数据结构的交互,并且会对数据进行处理以满足业务需求。
2. 示例

type OrderBO struct {
    items []Item
    customerID int
}

func (o *OrderBO) CalculateTotalPrice() float64 {
    total := 0.0
    for _, item := range o.items {
        total += item.Price * float64(item.Quantity)
    }
    return total
}

func (o *OrderBO) IsOrderValid() bool {
    // 这里可以添加更多复杂的订单有效性验证逻辑
    if len(o.items) == 0 {
        return false
    }
    return true
}

假设我们有一个在线购物系统,有一个表示订单的业务对象。这个BO可能包含计算订单总价、验证订单状态等业务逻辑。

二、VO(值对象)

1.概念
VOGo中是一种简单的值封装对象。它主要用于在不同的组件或者层之间传递数据,并且数据是不可变的(通常通过不提供修改方法来实现)。VO的目的是为了确保数据的完整性和一致性在传递过程中不被破坏。
2. 示例

type PriceQuantityVO struct {
    Price  float64
    Quantity int
}

比如在上述购物系统中,有一个表示商品价格和数量的组合值对象。

三、PO(持久化对象)

1.概念
Go中,PO是与数据库存储相关的对象。它的结构通常与数据库中的表结构相对应,并且包含了与数据库操作相关的标签或者方法,用于数据的持久化操作,如插入、查询、更新和删除等。
2.示例

假设我们有一个数据库表“users”,包含“id”、“name”、“email”等字段。对应的PO可能如下:

type UserPO struct {
    ID   int    `gorm:"primaryKey"`
    Name string `gorm:"column:name"`
    Email string `gorm:"column:email"`
}

如果使用Gorm(一个Go语言的ORM库),可以使用这个PO进行数据库操作,例如:

var user UserPO
db.First(&user, 1) // 查询id为1的用户

四、DTO(数据传输对象)

1.概念
DTOGo中用于在不同的系统、模块或者服务之间传输数据。它的设计目的是为了减少网络传输的数据量,只包含需要传输的必要数据,不包含业务逻辑。
2. 示例

type UserInfoDTO struct {
    Name string
    Email string
}

在我们的购物系统中,如果要将用户的基本信息传输给外部服务,可能会创建一个用户信息的DTO

一、Dsl

如之前理论部分介绍,Dsl具有如下信息

  1. Key,Version用于唯一标志一个Dsl
  2. Metadata 其余相关元信息
  3. DecisionFlow 决策流元信息(这里只包含节点的编排,即只有当前节点的名字、类型、下一个节点的名字、类型等信息,具体的节点运行时所需信息需要通过节点名字在后续字段关联上)
  4. Features 保存了该决策流所需的特征以及特征的值和类型等信息
  5. Abtests(AB概率分流节点)、Conditionals(条件分流节点)、Rulesets(规则集节点)、Trees\Matrixs(决策树与决策矩阵)、Scorecards(评分卡),他们都实现了INode接口,只是不同的节点类型罢了

注:字段的各种类型(如FlowNode,Feature,AbtestNode等),后续博客会详解

risk_engine/core/dsl.go

type Dsl struct {
	Key          string                 `yaml:"key"`
	Version      string                 `yaml:"version"`
	Metadata     map[string]interface{} `yaml:"metadata"`
	DecisionFlow []FlowNode             `yaml:"decision_flow,flow"`
	Features     []Feature              `yaml:"features,flow"`
	Abtests      []AbtestNode           `yaml:"abtests,flow"`
	Conditionals []ConditionalNode      `yaml:"conditionals,flow"`
	Rulesets     []RulesetNode          `yaml:"rulesets,flow"`
	Matrixs      []MatrixNode           `yaml:"matrixs,flow"`
	Trees        []TreeNode             `yaml:"trees,flow"`
	Scorecards   []ScorecardNode        `yaml:"scorecards,flow"`
}

为了简洁,后续博客可能主要会介绍下Abtests(AB概率分流节点)、Conditionals(条件分流节点)、Rulesets(规则集节点)的代码,Trees\Matrixs(决策树与决策矩阵)、Scorecards(评分卡)则可看仓库代码,原理是类似的,都是实现INode接口的方法,只是实现不同罢了。

Dsl的元信息从文件或者DB加载出来后,首先需要校验是否合法(是否具有Key以及决策流信息,可根据实际情况添加其他校验)

func (dsl *Dsl) CheckValid() bool {
	if dsl.Key == "" {
		return false
	}
	if len(dsl.DecisionFlow) == 0 {
		return false
	}
	return true
}

我们还需要提供将Dsl转为可执行图DecisionFlow的方法

//dsl to decisionflow
func (dsl *Dsl) ConvertToDecisionFlow() (*DecisionFlow, error) {
	flow := NewDecisionFlow()
	flow.Key = dsl.Key
	flow.Version = dsl.Version
	flow.Metadata = dsl.Metadata

	//map
	featureMap := make(map[string]IFeature)
	for _, feature := range dsl.Features {
		featureMap[feature.Name] = NewFeature(feature.Name, GetFeatureType(feature.Kind)) //IFeature
	}
	flow.FeatureMap = featureMap
	rulesetMap := make(map[string]INode)
	for _, ruleset := range dsl.Rulesets {
		rulesetMap[ruleset.GetName()] = ruleset
	}
	abtestMap := make(map[string]INode)
	for _, abtest := range dsl.Abtests {
		abtestMap[abtest.GetName()] = abtest
	}
	conditionalMap := make(map[string]INode)
	for _, conditional := range dsl.Conditionals {
		conditionalMap[conditional.GetName()] = conditional
	}
	matrixMap := make(map[string]INode)
	for _, martix := range dsl.Matrixs {
		matrixMap[martix.GetName()] = martix
	}
	treeMap := make(map[string]INode)
	for _, tree := range dsl.Trees {
		treeMap[tree.GetName()] = tree
	}
	scorecardMap := make(map[string]INode)
	for _, scorecard := range dsl.Scorecards {
		scorecardMap[scorecard.GetName()] = scorecard
	}

	//flow
	for _, flowNode := range dsl.DecisionFlow {
		newNode := flowNode //need set new variable
		switch GetNodeType(newNode.NodeKind) {
		case TypeRuleset:
			newNode.SetElem(rulesetMap[newNode.NodeName])
			flow.AddNode(&newNode)
		case TypeAbtest:
			newNode.SetElem(abtestMap[newNode.NodeName])
			flow.AddNode(&newNode)
		case TypeConditional:
			newNode.SetElem(conditionalMap[newNode.NodeName])
			flow.AddNode(&newNode)
		case TypeStart:
			newNode.SetElem(NewStartNode(newNode.NodeName))
			flow.SetStartNode(&newNode)
			flow.AddNode(&newNode)
		case TypeEnd:
			newNode.SetElem(NewEndNode(newNode.NodeName))
			flow.AddNode(&newNode)
		case TypeMatrix:
			newNode.SetElem(matrixMap[newNode.NodeName])
			flow.AddNode(&newNode)
		case TypeTree:
			newNode.SetElem(treeMap[newNode.NodeName])
			flow.AddNode(&newNode)
		case TypeScorecard:
			newNode.SetElem(scorecardMap[newNode.NodeName])
			flow.AddNode(&newNode)
		default:
			log.Warnf("dsl %s - %s convert warning: unkown node type %s", dsl.Key, dsl.Version, newNode.NodeKind)
		}
	}
	return flow, nil
}

二、INode接口

不同的节点类型都可统一编排到决策流中的重要条件,就是决策流依赖的是INode接口,具体的节点实现这个接口即可

risk_engine/core/inode.go

package core

import (
	"github.com/liyouming/risk_engine/configs"
)

type NodeInfo struct {
	Id      int64    `yaml:"id"`
	Name    string   `yaml:"name"`
	Tag     string   `yaml:"tag"`
	Label   string   `yaml:"label"`
	Kind    string   `yaml:"kind"`
	Depends []string `yaml:"depends,flow"`
}

// INode 各类型节点实现该接口
type INode interface {
	GetName() string
	GetType() NodeType
	GetInfo() NodeInfo
	BeforeParse(*PipelineContext) error
	Parse(*PipelineContext) (*NodeResult, error)
	AfterParse(*PipelineContext, *NodeResult) error
}

// NodeResult 节点返回内容 是否阻断 下一个节点信息(ab,条件节点)
type NodeResult struct {
	Id           int64
	Name         string
	Label        string
	Tag          string
	Kind         NodeType
	IsBlock      bool
	Score        float64
	Value        interface{}
	NextNodeName string //ab,条件节点有用
	NextNodeType NodeType
}

// all support node
type NodeType int

const (
	TypeStart NodeType = iota
	TypeEnd
	TypeRuleset
	TypeAbtest
	TypeConditional
	TypeTree
	TypeMatrix
	TypeScorecard
)

var nodeStrMap = map[NodeType]string{
	TypeStart:       configs.START,
	TypeEnd:         configs.END,
	TypeRuleset:     configs.RULESET,
	TypeAbtest:      configs.ABTEST,
	TypeConditional: configs.CONDITIONAL,
	TypeTree:        configs.DECISIONTREE,
	TypeMatrix:      configs.DECISIONMATRIX,
	TypeScorecard:   configs.SCORECARD,
}

func (nodeType NodeType) String() string {
	return nodeStrMap[nodeType]
}

var nodeTypeMap map[string]NodeType = map[string]NodeType{
	configs.START:          TypeStart,
	configs.END:            TypeEnd,
	configs.RULESET:        TypeRuleset,
	configs.ABTEST:         TypeAbtest,
	configs.CONDITIONAL:    TypeConditional,
	configs.DECISIONTREE:   TypeTree,
	configs.DECISIONMATRIX: TypeMatrix,
	configs.SCORECARD:      TypeScorecard,
}

func GetNodeType(name string) NodeType {
	return nodeTypeMap[name]
}

三、DecisionFlow

DecisionFlow 通过Run方法开始,执行一个决策流

  1. 找到开始节点,不断向后执行
  2. flowMap 存储了本决策流执行所需的所有节点信息
  3. FeatureMap则存着所有需要的特征信息

risk_engine/core/flow.go

package core

import (
	"errors"
	"fmt"
	"github.com/liyouming/risk_engine/internal/log"
)

type DecisionFlow struct {
	Key        string
	Version    string
	Metadata   map[string]interface{}
	Md5        string               // yaml文件的md5值
	flowMap    map[string]*FlowNode // 对应dsl中(yaml)文件中的DecisionFlow字段
	startNode  *FlowNode
	FeatureMap map[string]IFeature // 决策流依赖的相关特征
}

func NewDecisionFlow() *DecisionFlow {
	return &DecisionFlow{flowMap: make(map[string]*FlowNode)}
}

// AddNode 在一个决策流中,节点名字和节点类型唯一确定一个节点
func (flow *DecisionFlow) AddNode(node *FlowNode) {
	key := flow.getNodeKey(node.NodeName, node.NodeKind)
	if _, ok := flow.flowMap[key]; !ok {
		flow.flowMap[key] = node
	} else {
		log.Warnf("repeat add node %s", key)
	}
}

// NodeType string
func (flow *DecisionFlow) GetNode(name string, nodeType interface{}) (*FlowNode, bool) {
	key := flow.getNodeKey(name, nodeType)
	if flowNode, ok := flow.flowMap[key]; ok {
		return flowNode, ok
	}
	return new(FlowNode), false
}

func (flow *DecisionFlow) GetAllNodes() map[string]*FlowNode {
	return flow.flowMap
}

func (flow *DecisionFlow) getNodeKey(name string, nodeType interface{}) string {
	return fmt.Sprintf("%s-%s", nodeType, name)
}

func (flow *DecisionFlow) SetStartNode(startNode *FlowNode) {
	flow.startNode = startNode
}

func (flow *DecisionFlow) GetStartNode() (*FlowNode, bool) {
	if flow.startNode == nil {
		return &FlowNode{}, false
	}
	return flow.startNode, true
}

func (flow *DecisionFlow) Run(ctx *PipelineContext) (err error) {
	//recover
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered from panic: %v", r)
			log.Error(err)
		}
	}()

	//find StartNode
	flowNode, ok := flow.GetStartNode()
	if !ok {
		err = errors.New("no start node")
		return
	}

	gotoNext := true
	for gotoNext {
		//ctx.SetCurrentNode(flowNode)
		flowNode, gotoNext = flow.parseNode(flowNode, ctx)
	}
	return
}

// parse current node and return next node
func (flow *DecisionFlow) parseNode(curNode *FlowNode, ctx *PipelineContext) (nextNode *FlowNode, gotoNext bool) {
	//parse current node
	ctx.AddTrack(curNode.GetElem())
	res, err := curNode.Parse(ctx)
	ctx.AddNodeResult(curNode.NodeName, res)

	//error break
	if err != nil {
		log.Error(err)
		return
	}

	//node is block
	if res.IsBlock {
		gotoNext = !res.IsBlock
		return
	}

	//goto next node
	switch curNode.GetNodeType() {
	case TypeEnd: //END:
		gotoNext = false
		return
	case TypeConditional:
		fallthrough
	case TypeAbtest: //ABTEST
		nextNode, gotoNext = flow.GetNode(res.NextNodeName, res.NextNodeType)
		return
	default: //start,matrix,ruleset,tree,scorecard
		nextNode, gotoNext = flow.GetNode(curNode.NextNodeName, curNode.NextNodeKind)
		return
	}
}

type FlowNode struct {
	NodeName     string `yaml:"node_name"`
	NodeKind     string `yaml:"node_kind"`
	NextNodeName string `yaml:"next_node_name"`
	NextNodeKind string `yaml:"next_node_kind"`

	elem     INode
	nextNode *FlowNode
}

func (flowNode *FlowNode) GetNodeType() NodeType {
	return GetNodeType(flowNode.NodeKind)
}

func (flowNode *FlowNode) GetNextNodeType() NodeType {
	return GetNodeType(flowNode.NextNodeKind)
}

func (flowNode *FlowNode) SetElem(elem INode) {
	flowNode.elem = elem
}

func (flowNode *FlowNode) GetElem() INode {
	return flowNode.elem
}

func (flowNode *FlowNode) Parse(ctx *PipelineContext) (*NodeResult, error) {
	//before hook
	err := flowNode.elem.BeforeParse(ctx)
	if err != nil {
		return (*NodeResult)(nil), err
	}
	//parse
	result, err := flowNode.elem.Parse(ctx)
	if err != nil {
		return (*NodeResult)(nil), err
	}
	//after hook
	err = flowNode.elem.AfterParse(ctx, result)
	if err != nil {
		return (*NodeResult)(nil), err
	}
	return result, nil
}

四、Pipeline上下文

在策略执行的过程中,我们有很多的信息需要在整个决策流中流转,因此就有了上下文的定义,记录命中的规则、节点、节点的执行结果、特征的值等。

package core

import (
	"sync"
)

type PipelineContext struct {
	//request params

	//proccess result

	hMutex   sync.RWMutex
	hitRules map[string]*Rule // 命中了哪些规则

	tMutex sync.RWMutex
	tracks []*Track  // 经过了哪些节点

	nMutex      sync.RWMutex
	nodeResults map[string]*NodeResult  // 经过的节点的执行结果

	fMutex   sync.RWMutex
	features map[string]IFeature //保存所有上下文中已赋值的特征
}

type Track struct {
	Id    int64
	Name  string
	Label string
	Tag   string
	Kind  NodeType
}

func NewPipelineContext() *PipelineContext {
	return &PipelineContext{features: make(map[string]IFeature),
		hitRules:    make(map[string]*Rule),
		nodeResults: make(map[string]*NodeResult),
	}
}

func (ctx *PipelineContext) AddTrack(node INode) {
	ctx.tMutex.Lock()
	defer ctx.tMutex.Unlock()
	ctx.tracks = append(ctx.tracks, &Track{Name: node.GetName(),
		Id:    node.GetInfo().Id,
		Label: node.GetInfo().Label,
		Tag:   node.GetInfo().Tag,
		Kind:  node.GetType(),
	})
}

func (ctx *PipelineContext) GetTracks() []*Track {
	ctx.tMutex.RLock()
	defer ctx.tMutex.RUnlock()
	return ctx.tracks
}

func (ctx *PipelineContext) SetFeatures(features map[string]IFeature) {
	if len(features) == 0 {
		return
	}
	ctx.fMutex.Lock()
	defer ctx.fMutex.Unlock()
	for k, v := range features {
		ctx.features[k] = v //override the same key feature
	}
}

func (ctx *PipelineContext) SetFeature(feature IFeature) {
	ctx.fMutex.Lock()
	defer ctx.fMutex.Unlock()
	ctx.features[feature.GetName()] = feature //override the same key feature
}

func (ctx *PipelineContext) GetFeature(name string) (result IFeature, ok bool) {
	//local
	ctx.fMutex.RLock()
	localFeatures := ctx.features
	ctx.fMutex.RUnlock()
	if result, ok = localFeatures[name]; ok {
		return
	}
	//from remote

	return
}

func (ctx *PipelineContext) GetFeatures(depends []string) (result map[string]IFeature) {
	if len(depends) == 0 {
		return
	}

	//from local
	ctx.fMutex.RLock()
	localFeatures := ctx.features
	ctx.fMutex.RUnlock()

	result = make(map[string]IFeature)
	remoteList := make([]string, 0)
	for _, name := range depends {
		if v, ok := localFeatures[name]; ok {
			result[name] = v
		} else {
			remoteList = append(remoteList, name)
		}
	}

	//from remote
	if len(remoteList) == 0 {
		//远程调用后new的时候要知道类型
		return
	}

	//curl
	return
}

func (ctx *PipelineContext) GetAllFeatures() map[string]IFeature {
	ctx.fMutex.RLock()
	defer ctx.fMutex.RUnlock()
	return ctx.features
}

func (ctx *PipelineContext) AddHitRule(rule *Rule) {
	ctx.tMutex.Lock()
	defer ctx.tMutex.Unlock()
	ctx.hitRules[rule.Name] = rule
}

func (ctx *PipelineContext) GetHitRules() map[string]*Rule {
	ctx.tMutex.RLock()
	defer ctx.tMutex.RUnlock()
	return ctx.hitRules
}

func (ctx *PipelineContext) AddNodeResult(name string, nodeResult *NodeResult) {
	ctx.nMutex.Lock()
	defer ctx.nMutex.Unlock()
	ctx.nodeResults[name] = nodeResult
}

func (ctx *PipelineContext) GetNodeResults() map[string]*NodeResult {
	ctx.nMutex.RLock()
	defer ctx.nMutex.RUnlock()
	return ctx.nodeResults
}

type DecisionResult struct {
	HitRules    map[string]*Rule
	Tracks      []*Track
	Features    map[string]IFeature
	NodeResults map[string]*NodeResult
}

func (ctx *PipelineContext) GetDecisionResult() *DecisionResult {
	return &DecisionResult{
		HitRules:    ctx.GetHitRules(),
		Tracks:      ctx.GetTracks(),
		Features:    ctx.GetAllFeatures(),
		NodeResults: ctx.GetNodeResults(),
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值