后端开发
本文档详细介绍 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。
注意事项
- 必须调用 flag.Parse() - 在主程序开始时必须先调用
- 资源嵌入 - 使用
//go:embed
嵌入必要的资源文件