Hertz框架使用实例(规则引擎api服务作业) | ⻘训营笔记

215 阅读4分钟

这是我参与「第五届⻘训营 」笔记创作活动的第6天。今天是实践课程,学习利用Go语言构建规则引擎,这篇笔记记录了课后作业,结合前面课程的内容课后实现一个在线版本的规则引擎。

项目要求

使用Hertz框架开发一个HTTP服务,服务使用mysql,支持表达式的增删查改和编译执行。 并实现项目中预留的接口

项目目录

➜ young_engine main ✓ tree
.
├── README.md
├── biz
│   ├── dal
│   │   ├── dal.go
│   │   ├── expression.go
│   │   ├── expression_test.go
│   │   └── sql
│   │       └── init.sql
│   └── handler
│       ├── base.go
│       ├── compiler.go
│       ├── compiler_test.go
│       ├── engine.go
│       ├── expressions.go
│       └── ping.go
├── compiler
│   ├── Engine.g4
│   ├── builder.go
│   ├── lexical.go
│   ├── parser.go
│   ├── parser_test.go
│   ├── scanner.go
│   └── scanner_test.go
├── docker-compose.yml
├── executor
│   ├── ast.go
│   ├── operator.go
│   ├── svg.go
│   ├── symbol.go
│   ├── type.go
│   └── type_checker.go
├── go.mod
├── go.sum
├── image
│   ├── fixed.jpg
│   ├── fixed_point.jpg
│   ├── lexcer_status.jpg
│   ├── node.svg
│   └── precedence.jpg
├── main.go
├── setup.sh
├── test
│   ├── handle_test.go
│   └── rune_test.go
└── token
    ├── kind.go
    ├── kind_test.go
    ├── lexer.go
    └── token.go

我们要实现的代码在biz/handler文件夹的expressions.go中。

启动DB

使用docker-compose运行mysql作为DB,这是我们使用gorm操作数据的平台。

docker-compose up

运行项目

go run ./main.go

测试工具使用postman。

接口实现

查询表达式:

查询数据库中所有的表达式

  • GET api/engine/exp/list
  • Response
{
    "code": 0,
    "message": "success",
    "data": [  
        {
            "id": 1,
            "exp": "uid > 0"
        }
    ]
}

此接口为查询接口,使用dal包中的GetAllExpression()函数可从数据库中获取到查询的所有表达式,代码如下:

func HandleGetAllExpression(ctx context.Context, c *app.RequestContext) {
	exps, err := dal.GetAllExpression()
	if err != nil {
		BindResp(c, ParamErrCode, err.Error(), nil)
		return
	}
	BindResp(c, SuccessCode, SuccessMsg, exps)
}

从数据库中获取数据之后将获取结果map[string]interface{}填入返回报文body返回给客户端。

直接表达式执行

请求参数为待执行的表达式和表达式中参数的值,并输出编译结果

实时编译并执行结果,不需要写入DB中

  • POST api/engine/run
  • Request
{
    "exp": "uid == 12345 && did > 0",
    "params": {
        "uid": 123456,
        "did": 0
    }
}
复制代码
  • Response
{
    "code": 0,
    "message": "success",
    "data": {  // 执行结果
        "result": true
    }
}

实现代码如下:

func HandleRunExpression(ctx context.Context, c *app.RequestContext) {
	var req RunRequest
	if err := c.Bind(&req); err != nil {
		BindResp(c, ParamErrCode, err.Error(), nil)
		return
	}
	params, _ := getParams(req.Params)
	exps, _ := dal.GetExpressionByID(uint(req.Exp_id))

	if len(exps.Exp) == 0 {
		msg := fmt.Sprintf("exp id %d not exist", req.Exp_id)
		BindResp(c, DatabaseErrCode, msg, nil)
		return
	}

	evaluatedExp, err := Compiler(exps.Exp)
	if err != nil {
		BindResp(c, CompileErrCode, err.Error(), nil)
		return
	}

	err = evaluatedExp.Eval(params)
	if err != nil {
		BindResp(c, RuleExecErrCode, err.Error(), nil)
		return
	}

	resp, _ := evaluatedExp.GetVal()

	BindResp(c, SuccessCode, SuccessMsg, resp)

}

首先使用框架中实现的Bind机制,将请求body中json:exp表达式转换为自定义结构体

type RunRequest struct {
	Exp_id int                    `json:exp_id`
	Params map[string]interface{} `json:"parmas"`
}

根据获取结构体中Exp_id,使用dal包中getParams(id uint)函数根据id获取存在于DB中的表达式。如果获取正确使用先前构建好的规则引擎编译表达式,并执行结果,最终将结果存入返回body中回复给Client。

新增表达式

新增一条表达式到DB中,并返回表达式在DB中的ID

需要检测表达式是否已经存在,如果已经存在,直接返回表达式的ID

需要检测表达式是否合法(编译是否通过) ,如果编译失败,返回错误码 20001和编译错误

  • POST api/engine/exp/new
  • Request
{
    "exp": "uid == 12345 && did > 0",
}
复制代码
  • Response
{
    "code": 0,
    "message": "success",
    "data": {  // 表达式ID
        "id": 1
    }
}

// 编译失败时
{
    "code": -1,
    "message": "compile error: xxxxx", // 编译失败的信息
    "data": {  // 表达式ID
        "id": 0
    }
}

实现代码如下:

func HandleAddExpression(ctx context.Context, c *app.RequestContext) {
	var req AddRequst
	if err := c.Bind(&req); err != nil {
		BindResp(c, ParamErrCode, err.Error(), nil)
		return
	}

	resp := map[string]interface{}{
		"id": 0,
	}

	_, err := Compiler(req.Exp)
	if err != nil {
		BindResp(c, -1, err.Error(), resp)
		return
	}
	exps, err := dal.AddExpression(req.Exp)
	if err != nil {
		BindResp(c, DatabaseErrCode, err.Error(), resp)
		return
	}

	resp["id"] = exps.ID

	BindResp(c, SuccessCode, SuccessMsg, resp)
}

首先使用框架中实现的Bind机制,将请求body中json:exp表达式转换为自定义结构体

type AddRequst struct {
	Exp string `json:exp`
}

将获取到的表达式利用dal包中AddExpression(exp string)函数存入DB中,并将结果返回客户端。

删除表达式:

根据ID删除表达式,表达式不存在时返回错误码20002 , 和错误信息

删除成功返回被删除的表达式信息

  • DELETE api/engine/exp/:id
  • Response
// 删除成功时
{
    "code": 0,
    "message": "success",
    "data": {  // 表达式ID
        "id": 1,
        "exp": "uid > 0"
    }
}

// 删除失败时
{
    "code": -1,
    "message": "exp id 1 not exist", //查询失败的信息
    "data": {}
}

实现代码如下:

func HandleDeleteExpression(ctx context.Context, c *app.RequestContext) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		BindResp(c, ParamErrCode, err.Error(), nil)
		return
	}
	exps, _ := dal.GetExpressionByID(uint(id))
	if len(exps.Exp) == 0 {
		msg := fmt.Sprintf("exp id %d not exist", id)
		BindResp(c, DatabaseErrCode, msg, nil)
		return
	}
	err = dal.DeleteExpressionByID(uint(id))
	if err != nil {
		BindResp(c, DatabaseErrCode, "failed delete Exp from database", nil)
		return
	}
	BindResp(c, SuccessCode, SuccessMsg, exps)
}

实现较为简单,首先查询数据库中是否存在id对应的数据,如果不存在,则返回删除错误信息,存在则将其id以及表达式作为返回体返回。