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

入口文件

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接口路由,必填
	})
}

插件api路由注册

文件地址: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"
)

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

插件数据持久化

查询操作

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

这是插件版本升级时,表结构需要变更的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...)

获取数据源类型

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

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

res,err := api.DsType()


/***
数据源类型目前有
ElasticSearch6 = "elasticsearch6.x"
ElasticSearch7 = "elasticsearch7.x"
ElasticSearch8 = "elasticsearch8.x"
Mysql = "mysql"
Redis = "redis"
ClickHouse = "clickhouse"
Postgres = "postgres"
Mongo = "mongo"
Dameng = "dameng"
Oracle = "oracle"
Sqlserver = "sqlserver"
Mariadb = "mariadb"
Hive = "hive"
 */

Elasticsearch数据源操作

获取es版本

go

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

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

// 获取ES版本(注意,虽然提供版本查询,但是以下文档所示api已做多版本适配,无特殊情况不用对版本进行特殊处理)
version, err := api.EsVersion()

创建索引

go
res, err := api.EsCreateIndex(ctx, proto2.IndicesCreateRequest{
    Index: "index_name",
    Body:  indexMapping, // 索引映射配置
})

删除索引

go
resp, err := api.EsDeleteIndex(ctx, proto2.IndicesDeleteRequest{
    Index: []string{"index_name"},
})

获取索引设置

go
res, err := api.EsIndicesGetSettingsRequest(ctx, proto2.IndicesGetSettingsRequest{
    Index: []string{"index_name"},
})

更新索引设置

go
res, err := api.EsIndicesPutSettingsRequest(ctx, proto2.IndicesPutSettingsRequest{
    Index: []string{"index_name"},
    Body:  settingsBody,
})

获取索引映射

go
resp, err := api.EsGetMapping(ctx, []string{"index_name"})

更新索引映射

go
resp, err := api.EsPutMapping(ctx, proto2.IndicesPutMappingRequest{
    Index: []string{"index_name"},
    Body:  mappingBody,
})

重建索引

go
reindexRes, err := api.EsReindex(ctx, reindexRequest, reindexBody)

索引操作

go
// 刷新索引
resp, err := api.EsRefresh(ctx, indexNames)

// 清空缓存
resp, err := api.EsIndicesClearCache(ctx, indexNames)

// 关闭索引
resp, err := api.EsIndicesClose(ctx, indexNames)

// 打开索引
resp, err := api.EsOpen(ctx, indexNames)

// 强制合并
resp, err := api.EsIndicesForcemerge(ctx, indexNames, &maxNumSegments)

// 刷新到磁盘
resp, err := api.EsFlush(ctx, indexNames)

// 按查询删除
resp, err := api.EsDeleteByQuery(ctx, indexNames, nil, deleteQuery)

添加别名到索引

go
resp, err := api.EsAddAliases(ctx, []string{"index_name"}, "alias_name")

批量添加别名

go
resp, err := api.EsAddAliases(ctx, []string{"index_name"}, aliasName)

移除别名

go
resp, err := api.EsRemoveAliases(
    ctx,
    []string{"index_name"},
    []string{"alias_name"},
)

获取索引别名

go
aliasRes, err := api.EsGetAliases(ctx, []string{"index_name"})

移动别名到另一个索引

go
_, err = api.EsMoveToAnotherIndexAliases(
    ctx,
    proto.AliasAction{Actions: []proto.AliasAddAction{
        {
            Add: proto.AliasAdd{
                Indices: newIndexList,
                Alias:   aliasName,
            },
        },
    }})

删除文档

go
res, err := api.EsDelete(ctx, proto2.DeleteRequest{
    Index:        "index_name",
    DocumentType: "doc_type",
    DocumentID:   "document_id",
})

更新文档

go
res, err := api.EsUpdate(ctx, proto2.UpdateRequest{
    Index:        "index_name",
    DocumentType: "doc_type",
    DocumentID:   "document_id",
}, updateJSON)

创建文档

go
resp, err := api.EsCreate(ctx, proto2.CreateRequest{
    Index:        "index_name",
    DocumentType: "doc_type",
    DocumentID:   "document_id",
}, documentJSON)

搜索文档

go
resp, err := api.EsSearch(
    ctx,
    proto2.SearchRequest{Index: []string{"index_name"}},
    searchBody,
)

批量删除文档

go
// 构建批量删除请求
var buf bytes.Buffer
for _, id := range ids {
    meta := fmt.Sprintf(`{ "delete" : { "_index" : "%s", "_id" : "%s" } }`, index, id)
    buf.WriteString(meta + "\n")
}

req, err := http.NewRequest(http.MethodPost, "/_bulk", strings.NewReader(buf.String()))
req.Header.Set("Content-Type", "application/x-ndjson")

res, err := api.EsPerformRequest(ctx, req)

集群健康状态

go
res, err = api.EsCatHealth(ctx, proto2.CatHealthRequest{
    Format: "json",
    Human:  true,
})

分片信息

go
res, err = api.EsCatShards(ctx, proto2.CatShardsRequest{
    Format: "json",
    Human:  true,
})

文档数量统计

go
res, err = api.EsCatCount(ctx, proto2.CatCountRequest{
    Format: "json",
    Human:  true,
})

节点分配信息

go
res, err = api.EsCatAllocationRequest(ctx, proto2.CatAllocationRequest{
    Format: "json",
    Human:  true,
})

别名信息

go
res, err = api.EsCatAliases(ctx, proto2.CatAliasesRequest{
    Format: "json",
    Human:  true,
})

节点信息

go
res, err = api.EsCatNodes(ctx, strings.Split("ip,name,heap.percent,heap.current,heap.max,ram.percent,ram.current,ram.max,node.role,master,cpu,load_1m,load_5m,load_15m,disk.used_percent,disk.used,disk.total", ","))

索引信息

go
catIndicesResponse, err := api.EsGetIndices(ctx, proto2.CatIndicesRequest{
    Format: "json",
    Human:  true,
    Index:  []string{"*,-.*"}, // 排除系统索引
})

分片段信息

go
res, err = api.EsIndicesSegmentsRequest(ctx, true)

集群统计

go
res, err = api.EsClusterStats(ctx, true)

获取快照仓库

go
res, err := api.EsSnapshotGetRepository(ctx, repositoryNames)

创建快照仓库

go
res, err := api.EsSnapshotCreateRepository(ctx, repositoryName, repositoryBody)

删除快照仓库

go
res, err := api.EsSnapshotDeleteRepository(ctx, []string{repositoryName})

创建快照

go
esRes, err := api.EsSnapshotCreate(
    ctx,
    repositoryName,
    snapshotName,
    snapshotBody,
)

获取快照状态

go
esRes, err := api.EsSnapshotStatus(
    ctx,
    repositoryName,
    snapshotName,
)

删除快照

go
esRes, err := api.EsSnapshotDelete(ctx, repositoryName, snapshotName)

恢复快照

go
esRes, err := api.EsRestoreSnapshot(
    ctx,
    repositoryName,
    snapshotName,
    restoreBody,
)

获取任务列表

go
resp, err := api.EsTaskList(ctx)

取消任务

go
res, err := api.EsTasksCancel(ctx, taskID)

执行自定义请求

go
res, err := api.EsPerformRequest(ctx, httpRequest)

SQL类型数据库数据源操作

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

查询多条记录 参数从左到右分别为 上下文,数据库名,执行sql语句,sql参数

go
columns, result, err := api.MysqlSelectSql(ctx,"my_database", 
	"SELECT * FROM products WHERE price >= ? and price <= ? ",100,110})

查询单条记录 参数从左到右分别为 上下文,数据库名,执行sql语句,sql参数

go
result, err := api.MysqlFirstSql(ctx, "my_database",
"SELECT * FROM users WHERE id = ?",userId,
})

执行增删改的SQL 参数从左到右分别为 上下文,数据库名,执行sql语句,sql参数

go
rowsAffected, err := api.MysqlExecSql(ctx, "my_database",
"UPDATE products SET price = 240 WHERE category = ?","electronics",
})

批量插入数据 参数从左到右分别为 上下文,数据库名,表名/集合名,插入字段列表,数据数组:

go
err := api.BatchInsertData(ctx, "db", "table", []string{
"col1",
}, [][]interface{}{
{
"加油",
},{
"成功",
}
})

Redis操作

通过key获取value

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"})

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

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

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

*/

新增一个延迟删除的key

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

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

Redis Hash操作

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

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

MongoDB操作

go
import (
	"github.com/1340691923/eve-plugin-sdk-go/ev_api"
	"github.com/1340691923/eve-plugin-sdk-go/ev_api/bson"
	"github.com/1340691923/eve-plugin-sdk-go/util"
)

// 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插入多条文档命令 参数从左到右分别为 上下文,数据库名,多条文档内容

go
res, err := api.MongoInsertManyDocuments(ctx, "test", "dt_world_chat",
				[]bson.M{
					bson.M{
						"msg":            "666",
						"send_time":      "17:43",
						"send_user_name": "散人55",
						"send_user_id":   "orBdl5HIW_V1kMQbA5mUT4h5YzLI",
						"avatar":         "",
						"time":           time.Now(),
					},
					bson.M{
						"msg":            "222",
						"send_time":      "17:43",
						"send_user_name": "散人55",
						"send_user_id":   "orBdl5HIW_V1kMQbA5mUT4h5YzLI",
						"avatar":         "",
						"time":           time.Now(),
					},
				})

执行mongodb插入文档命令 参数从左到右分别为 上下文,数据库名,文档内容

go
res, err := api.MongoInsertDocument(ctx, "test", "dt_world_chat",
				bson.M{
					"msg":            "666",
					"send_time":      "17:43",
					"send_user_name": "散人55",
					"send_user_id":   "orBdl5HIW_V1kMQbA5mUT4h5YzLI",
					"avatar":         "",
					"time":           "2025-07-21T15:31:39.226+08:00",
				})

执行mongodb修改文档命令 参数从左到右分别为 上下文,数据库名,集合名,文档id(非id过滤修改时传nil),修改命令

go
//返回 匹配条数,已修改条数,upserted条数,upsertedID,err
update := bson.M{
				"$set": bson.M{"time": time.Now().AddDate(0, 0, -1)},
			}

matchedCount , modifiedCount , upsertedCount , upsertedID, err := api.MongoUpdateDocument(ctx, "test", "dt_world_chat",
				nil, bson.M{
					"time": bson.M{
						"$gte": time.Now().AddDate(0, 0, -1),
					},
				}, update)

执行mongodb聚合查询命令,入参是 ctx,数据库名,集合名,bson命令

go
pipeline2 := bson.Pipeline{
				{
					{"$group", bson.M{
						"_id":   "$msg",
						"count": bson.M{"$sum": 1},
					}},
				},
				{
					{"$count", "total_distinct_msgs"},
				},
			}

res, err := api.MongoAggregateDocuments(ctx, "test", "dt_world_chat",pipeline2)

执行mongodb查询命令,入参是 ctx,数据库名,集合名,projection,filter,_id,skip,page

go
filter := bson.M{"_id": "6597cf39014ee43759061433"}

res, err := api.MongoFindDocuments(ctx, "test", "dt_world_chat",
		bson.M{"_id": 1}, filter, nil, 0, 0)

执行mongodb删除命令,入参是 ctx,数据库名,集合名,_id,filter的bson

go
res, err := api.MongoDeleteDocument(ctx, "test", "dt_world_chat", "6597b53f014ee43759061141",nil)

执行mongodb总数查询命令,入参是 ctx,数据库名,集合名,filter的bson

go
res,err := api.MongoCountDocuments(ctx,"test","dt_world_chat",nil)

执行mongodb总数查询命令,入参是 ctx,数据库名,集合名,filter的bson

go
res,err := api.MongoCountDocuments(ctx,"test","dt_world_chat",nil)

获取mongodb数据库列表,入参是 ctx

go
res,err := api.ShowMongoDbs(ctx)

获取mongodb集合列表,入参是 ctx,数据库名称

go
res,err := api.MongoGetCollections(ctx,dbName)

批量插入数据 参数从左到右分别为 上下文,数据库名,表名/集合名,插入字段列表,数据数组:

go
err := api.BatchInsertData(ctx, "db", "table", []string{
"col1",
}, [][]interface{}{
{
"加油",
},{
"成功",
}
})

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

A: 可能是因为plugin.json中的frontend_debug选项没有设置为true,或者plugin.json配置的菜单icon不是elementui的icon。

注意事项

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

下一步