Skip to content

后端开发

本文档详细介绍 ElasticView 插件后端开发的架构设计、API 开发、数据库操作和最佳实践。

技术栈

  • Go 1.20+ - 编程语言
  • eve-plugin-sdk-go - ElasticView 官方后端 SDK
  • Gin - HTTP Web 框架(SDK内置)
  • SQLite/MySQL - 数据库支持

项目结构

插件项目根目录/
├── backend/                    # 后端代码目录
│   ├── router/                 # 路由配置
│   │   └── router.go          # 路由注册文件
│   ├── migrate/                # 数据库迁移(可选)
│   │   └── v0_0_1.go          # 版本迁移文件
│   └── main.go                 # 程序入口
├── frontend/                   # 前端代码目录
├── plugin.json                 # 插件配置文件(必需)
├── logo.png                   # 插件图标(必需)
├── go.mod
├── go.sum
└── README.md

快速开始

1. 程序入口(main.go)

go
package main

import (
	"context"
	"embed"
	_ "embed"
	"ev-plugin/backend/migrate"
	"ev-plugin/backend/router"
	"ev-plugin/frontend"
	"flag"
	"github.com/1340691923/eve-plugin-sdk-go/backend/plugin_server"
	"github.com/1340691923/eve-plugin-sdk-go/build"
)

//go:embed plugin.json
var pluginJsonBytes []byte

//go:embed logo.png
var logoPng embed.FS

func main() {
	// 必须先解析参数
	flag.Parse()
	
	// 启动插件服务
	plugin_server.Serve(plugin_server.ServeOpts{
		Assets: &plugin_server.Assets{
			PluginJsonBytes: pluginJsonBytes, // plugin.json 插件配置,必填
			FrontendFiles:   frontend.StatisFs, // 前端工程编译后产物,必填
			Icon: logoPng, // logo图标,必填
		},
		ReadyCallBack: func(ctx context.Context) {
			// 插件就绪后的回调,可以在这里进行初始化操作,可以监听ctx管道从而实现优雅退出
		},
		Migration: &build.Gormigrate{Migrations: []*build.Migration{
			migrate.V0_0_1(),
		}}, // 插件存储的版本迁移配置,可选
		RegisterRoutes: router.NewRouter, // 注册插件http接口路由,必填
	})
}

2. 路由注册(router/router.go)

go
package router

import (
	"github.com/1340691923/eve-plugin-sdk-go/backend/web_engine"
	"github.com/1340691923/eve-plugin-sdk-go/ev_api"
	"github.com/gin-gonic/gin"
	"net/http"
)

// NewRouter 注册插件路由
func NewRouter(engine *web_engine.WebEngine) {
	// 创建API路由组
	userGroup := engine.Group("用户管理", "/api/user")
	
	// 参数从左到右依次为:是否鉴权,接口备注,路由path,handler
	userGroup.POST(true, "获取用户列表", "/list", getUserList)
	userGroup.POST(true, "创建用户", "/create", createUser)
	
	// 公开API(不需要鉴权)
	publicGroup := engine.Group("公开接口", "/api/public")
	publicGroup.POST(false, "健康检查", "/health", healthCheck)
}

// handle处理函数示例
func getUserList(c *gin.Context) {
	// 获取EV API实例
	api := ev_api.GetEvApi()
	
	// 查询用户列表
	var users []User
	err := api.StoreSelect(c.Request.Context(), &users, 
		"SELECT * FROM users WHERE status = ? ORDER BY created_at DESC", 1)
	
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"msg":  err.Error(),
			"code": 500,
		})
		return
	}
	//code 为 0 为成功,非0为异常
	c.JSON(http.StatusOK, gin.H{
		"code": 0,
		"data": users,
		"msg":  "success",
	})
}

func createUser(c *gin.Context) {
	api := ev_api.GetEvApi()
	
	var req CreateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 400,
			"msg":  "参数错误: " + err.Error(),
		})
		return
	}
	
	// 插入用户数据
	err := api.StoreSave(c.Request.Context(), "users", map[string]interface{}{
		"name":  req.Name,
		"email": req.Email,
		"age":   req.Age,
	})
	
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 500,
			"msg":  "创建失败: " + err.Error(),
		})
		return
	}
	
	c.JSON(http.StatusOK, gin.H{
		"code": 0,
		"msg":  "创建成功",
	})
}

func healthCheck(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"code":   0,
		"msg":    "OK",
		"status": "healthy",
	})
}

// 请求结构体
type CreateUserRequest struct {
	Name  string `json:"name" binding:"required"`
	Email string `json:"email" binding:"required,email"`
	Age   int    `json:"age" binding:"min=1,max=120"`
}

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
	Age   int    `json:"age"`
}

插件数据持久化操作(该api是用于插件数据存储,而非数据源操作)

基础操作

查询操作

go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"

// 获取全局EV API实例(内部已处理成单例)
api := ev_api.GetEvApi()

// 查询多条记录
var users []User
err := api.StoreSelect(ctx, &users, "SELECT * FROM users WHERE age > ?", 18)

// 查询单条记录
var user User
err := api.StoreFirst(ctx, &user, "SELECT * FROM users WHERE id = ?", userId)

执行操作

go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"

// 获取全局EV API实例(内部已处理成单例)
api := ev_api.GetEvApi()

// 执行SQL语句(INSERT、UPDATE、DELETE) 参数从左到右是 上下文,SQL,SQL执行参数
rowsAffected, err := api.StoreExec(ctx, "UPDATE users SET name = ? WHERE id = ?", "新名称", userId)

// 批量执行(内部有事务处理,要么全执行成功,要么全执行失败)
sqls := []dto.ExecSql{
    {Sql: "INSERT INTO users (name) VALUES (?)", Args: []interface{}{"用户1"}},
    {Sql: "INSERT INTO users (name) VALUES (?)", Args: []interface{}{"用户2"}},
}
err := api.StoreMoreExec(ctx, sqls)

便捷操作 类orm操作

go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"

// 获取全局EV API实例(内部已处理成单例)
api := ev_api.GetEvApi()
// 保存数据 参数从左到右是 上下文,表名,实体数据
err := api.StoreSave(ctx, "tableName", map[string]interface{}{
    "name":  "张三",
    "email": "zhangsan@example.com",
    "age":   25,
})

// 更新数据 参数从左到右是 上下文,表名,需修改实体数据,where条件SQL,SQL执行参数(对应 ? 传参)
rowsAffected, err := api.StoreUpdate(ctx, "tableName", 
    map[string]interface{}{"name": "李四"}, 
    "id = ?", userId)

// 删除数据 参数从左到右是 上下文,表名,where条件SQL,SQL执行参数(对应 ? 传参)
rowsAffected, err := api.StoreDelete(ctx, "users", "id = ?", userId)

// 插入或更新(Upsert) 参数从左到右是 上下文,表名,需插入or更新实体数据,唯一键字段
err := api.StoreInsertOrUpdate(ctx, "tableName", 
    map[string]interface{}{
        "setting_key":   "theme",
        "setting_value": "dark",
    }, "setting_key")

插件版本升级时表结构需要变更的migrate操作

如果需要数据存储,可以创建版本迁移文件:

go
// migrate/v0_0_1.go
package migrate

import "github.com/1340691923/eve-plugin-sdk-go/build"

// V0_0_1 创建版本迁移,分别为 sqlite 和 mysql
func V0_0_1() *build.Migration {
	return &build.Migration{
		ID: "v0.0.1", // 版本号
		SqliteMigrateSqls: []*build.ExecSql{ //当ev存储类型为sqlite时执行
			{
				Sql: `CREATE TABLE IF NOT EXISTS user_settings (
					id INTEGER PRIMARY KEY AUTOINCREMENT,
					user_id INTEGER NOT NULL,
					setting_key TEXT NOT NULL,
					setting_value TEXT,
					created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
					updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
				)`,
				Args: []interface{}{},
			},
			{
				Sql: "CREATE INDEX idx_user_settings_user_id ON user_settings(user_id)",
				Args: []interface{}{},
			},
		},
		MysqlMigrateSqls: []*build.ExecSql{//当ev存储类型为mysql时执行
			{
				Sql: `CREATE TABLE IF NOT EXISTS user_settings (
					id INT AUTO_INCREMENT PRIMARY KEY,
					user_id INT NOT NULL,
					setting_key VARCHAR(255) NOT NULL,
					setting_value TEXT,
					created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
					updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
				) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
				Args: []interface{}{},
			},
			{
				Sql: "CREATE INDEX idx_user_settings_user_id ON user_settings(user_id)",
				Args: []interface{}{},
			},
		},
		SqliteRollback: []*build.ExecSql{
			{Sql: "DROP TABLE IF EXISTS user_settings", Args: []interface{}{}},
		},
		MysqlRollback: []*build.ExecSql{
			{Sql: "DROP TABLE IF EXISTS user_settings", Args: []interface{}{}},
		},
	}
}

//V0_0_1用于 plugin_server.ServeOpts的Migration属性中
func main() {

	// 启动插件服务
	plugin_server.Serve(plugin_server.ServeOpts{
		...
		Migration: &build.Gormigrate{Migrations: []*build.Migration{
			migrate.V0_0_1(),
		}} // 插件存储的版本迁移配置,可选
	})
}

SQL构建工具

SDK提供了便捷的SQL构建器,可实现类orm操作:

go
import "github.com/1340691923/eve-plugin-sdk-go/sql_builder"

// 使用SQL构建器
query := sql_builder.SqlBuilder.
    Select("id", "name", "email").
    From("users").
    Where(sql_builder.And{
        sql_builder.Eq{"status": 1},
        sql_builder.Gt{"age": 18},
    }).
    OrderBy("created_at DESC").
    Limit(10).
    Offset(sql_builder.CreatePage(2, 10)) // 第2页,每页10条

sql, args, err := query.ToSql()
if err != nil {
    // 处理错误
}

// 执行查询
var users []User
err = api.StoreSelect(ctx, &users, sql, args...)

第三方数据源操作

Elasticsearch操作

go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"

// dataSourceId 为数据源id,一般为插件前端传递的 sdk.GetSelectEsConnID()
api := ev_api.NewEvWrapApi(dataSourceId, util.GetEvUserID(ctx))

// 获取ES版本
version, err := api.EsVersion()

// ES搜索操作
searchRes, err := api.EsSearch(ctx, dto.SearchReq{
    SearchReqData: dto.SearchReqData{
        SearchRequest: proto.SearchRequest{
            Index: []string{"my-index"},
            Size:  proto.IntPtr(100),
            From:  proto.IntPtr(0),
        },
        Query: map[string]interface{}{
            "match_all": map[string]interface{}{},
        },
    },
})

// 复杂查询示例
complexSearchRes, err := api.EsSearch(ctx, dto.SearchReq{
    SearchReqData: dto.SearchReqData{
        SearchRequest: proto.SearchRequest{
            Index: []string{"logs-*"},
            Size:  proto.IntPtr(50),
        },
        Query: map[string]interface{}{
            "bool": map[string]interface{}{
                "must": []interface{}{
                    map[string]interface{}{
                        "range": map[string]interface{}{
                            "@timestamp": map[string]interface{}{
                                "gte": "now-1h",
                            },
                        },
                    },
                    map[string]interface{}{
                        "term": map[string]interface{}{
                            "level": "ERROR",
                        },
                    },
                },
            },
        },
    },
})

SQL类型数据库数据源操作,该api操作可用于以下数据源(mysql,clickhouse,达梦,hive,MariaDB,oracle,Postgresql,sqlserver)

go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"

// dataSourceId 为数据源id,一般为插件前端传递的 sdk.GetSelectEsConnID()
api := ev_api.NewEvWrapApi(dataSourceId, util.GetEvUserID(ctx))


// MySQL查询多条记录 参数从左到右分别为 上下文,数据库名,执行sql语句,sql参数
columns, result, err := api.MysqlSelectSql(ctx,"my_database", 
	"SELECT * FROM products WHERE price >= ? and price <= ? ",100,110})

// MySQL查询单条记录 参数从左到右分别为 上下文,数据库名,执行sql语句,sql参数
result, err := api.MysqlFirstSql(ctx, "my_database", 
	"SELECT * FROM users WHERE id = ?",userId,
})

// MySQL执行增删改的SQL  参数从左到右分别为 上下文,数据库名,执行sql语句,sql参数
rowsAffected, err := api.MysqlExecSql(ctx, "my_database",
	"UPDATE products SET price = 240 WHERE category = ?","electronics",
})

Redis操作

go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"

// dataSourceId 为数据源id,一般为插件前端传递的 sdk.GetSelectEsConnID()
api := ev_api.NewEvWrapApi(dataSourceId, util.GetEvUserID(ctx))

// Redis命令执行 参数从左到右分别为 上下文,数据库名,Redis命令参数
data, err := api.RedisExecCommand(ctx,0,"GET", "user:123"})

// Redis SET操作 参数从左到右分别为 上下文,数据库名,Redis命令参数
data, err := api.RedisExecCommand(ctx, 0,"SET", "user:123", "张三", "EX", 3600)

// Redis Hash操作 参数从左到右分别为 上下文,数据库名,Redis命令参数
data, err := api.RedisExecCommand(ctx, 0, "HGETALL", "user:profile:123")

/***
data 响应值类型为interface{} 以上代码相等于

import "github.com/go-redis/redis/v8"

data,err = redis.NewClient(options).Do(ctx, args...).Result()

 */

MongoDB操作

go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"

// dataSourceId 为数据源id,一般为插件前端传递的 sdk.GetSelectEsConnID()
api := ev_api.NewEvWrapApi(dataSourceId, util.GetEvUserID(ctx))

// 执行MongoDB查询命令 参数从左到右分别为 上下文,数据库名,MongoDB命令,超时时间
result, err := api.ExecMongoCommand(ctx, 
   "dbname", bson.D{
        {"find", "users"},
        {"filter", bson.D{
            {"age", bson.D{{"$gt", 18}}},
            {"status", "active"},
        }},
        {"limit", 100},
    }, 30 * time.Second,
})

// MongoDB聚合查询 参数从左到右分别为 上下文,数据库名,MongoDB命令,超时时间
result, err := api.ExecMongoCommand(ctx,
   "dbname", bson.D{
        {"aggregate", "orders"},
        {"pipeline", bson.A{
            bson.D{{"$match", bson.D{{"status", "completed"}}}},
            bson.D{{"$group", bson.D{
                {"_id", "$category"},
                {"total", bson.D{{"$sum", "$amount"}}},
                {"count", bson.D{{"$sum", 1}}},
            }}},
            bson.D{{"$sort", bson.D{{"total", -1}}}},
        }},
        {"cursor", bson.D{}},
    }, 60 * time.Second,
})

Q: 为什么我的代码没有问题,但是页面一片空白?

A: 可能是因为plugin.json中的frontend_debug选项没有设置为true。

注意事项

  1. 必须调用 flag.Parse() - 在主程序开始时必须先调用
  2. 资源嵌入 - 使用 //go:embed 嵌入必要的资源文件

下一步